Get API data when visible?

Not sure if there is a way to fetch data when the component is about to be visible in the browser when scrolling?

Scenario: I have been trying to get my dashboard to load faster and tried
1.) asyncData and push the data to the individual components as props (1 http request)
2.) fetch and then push data to Vuex and fill each component store for the dashboard (1 http request)
3.) let each component independently get its own data on mounted hook (10+ http requests)

Tried a combo of each with async for the components above the fold as 1 http request and then mounted hook for the components below the page fold to just run in the background unseen, and still nothing to suit my happiness in terms of load time. A pretty full dashboard data come in around 130 -150kb with fetch time from ~150ms to ~450ms its the time pushing the data to the components then render that’s causing my displeasure…:slight_smile:

When I say dashboard and each component I mean this image as this as a mocked example.

Where each “block” is its own component on the dashboard so in this image there are 10 “blocks / components” each with its own Vuex store, manages its own data, has its individual API endpoints.

So i was wondering is there a way to say let any components under the page fold to do nothing. And when user scrolls the page and the components are in view or about to be in view, then they make the request to fetch data. So say the 10 you see in the image are above the fold, but there are 10 more below also in this scenario. Those 10 are “dead” until user scrolls and are about to come into view then somehow tell them its time to fetch your data type of idea.

Certainly. With the IntersectionObserver API this is quite straightforward. As long as you don’t need IE support, you’re golden. There is a polyfill if you do though.

Something like this might be an ideal helper

export function observe (target, callback, threshold = 1) {
  const fn = (entries) => entries.forEach(el => {
    if (el.isIntersecting && el.intersectionRatio > 1) {      
      observer.unobserve(target);

      if (callback && typeof callback === 'function') return callback();
    }
  });

  const observer = new IntersectionObserver(fn, {
    threshold: threshold
  });

  observer.observe(target);

  return observer;
}

There’s also a directive plugin if you prefer that route.

1 Like

Thanks,

I will give it a try and see how it goes.

Do you think this is a good solution to my issue or is there something I should be trying instead of this?

OK I used the directive plugin you suggested.
The plugin works fine but trying to figure out for my implementation issues.

If I fetch data when the plugin is about to be in view, its have lots of console undefined since the component is still being rendered below the fold but its got no data that it is expecting.

I cant hide the component till its about to be visible since if its hidden the plugin can see it to know its there.

A snip of my dashboard is like so:

<v-flex xs12>
  <v-layout row wrap>
    <v-flex xs12 lg4>
      <service-areas-mgt 
         v-observe-visibility="visibilityServiceArea"
         :visibility="serviceAreaVisible"/> // hide me till visible in viewport
    </v-flex>
    <v-flex xs12 lg4>
      <schedule-mgt /> // hide me till visible in viewport
    </v-flex>
    <v-flex xs12 lg4>
      <contact-mgt /> // hide me till visible in viewport
    </v-flex>
  </v-layout>
</v-flex>

So where is best place to set the visibility? On the component or in the component?

Thanks

I moved it to inside the component.

Seems to be working as planned.

I did the hide until visible in the individual components below the fold and when scrolled into visible it actually takes much longer…

Each {name}-mgt component is a wrapper / shell for the internal content which is dynamically loading 1 of 2 components. They all follow same structure with a

<component :is="currentComponent" />

So the initial content is a table of data, and there is a max of 10 table rows so its not a whole lot of data, and its all in Vuex already when the page is loaded its pushed to Vuex. So there is no API calls to get the data for the hidden components, its the render of the component when visible now that lags in the browser when scrolling.

Inspecting dev tools Vuex tab the commits (16 commits, one for each component / module individual store module from 1 http call ~300 - 400ms) to the store start at 16:15:22:405 and finish’s at 16:15:23:261 so less than 1 second to completely fill the store with the 130kb of data so its not Vuex holding the site up.
There is 1 image only on the dashboard which is on the large side ~300kb but its (disc cached)

Even comment out the image part and its still taking much longer than I would like.
I have a separate page you navigate to “media” with 50 images to test load time and it loads in less than a second all images rendered and page is fully interactive with no lag time

But still running around 4 to 5 seconds for full render on the dashboard and user interaction is allowed.

Anyone have any ideas how to speed up a site? Pitfalls to look out for? Just lost for ideas since I have tried so many ways to circumvent this issue and all seem to make zero significant impact or some even make it worse.

I don’t think it’s possible for us to know without seeing a replication of the issue or at least all the code in question. My understanding was that you wanted to fire endpoint requests only when the component enters the view (to save on number of initial requests during page load).

Most dashboards will load each component individually and display a loading indicator until its received its data. There’s also the question of what each component displays. Graphs and charts do take some time to render and if you’re rendering them all at once then that could cause visible lag… but again, this is all speculation without seeing the code in question.

I understand completely. There is unfortunately no live demo server as this project is not on a public server at this time to simply send a link.

I can say no charts or graphs or anything that requires any type of intensive processing to render the data. Its 100% text from the API with any computed (date modification is the most intense modification to avoid having the browser change dates / timestamps into a readable Monday Sept 1st, 2019 type thing is all done server side)

As stated, I tried with each module / component to fetch each on its own mounted / beforeMount call so it shows the loading icon as you pointed out. But still tried many different approaches and still looking for best solution.

I did initially want to have the ones as scrolled into view make a API call on their own. But was getting a load of undefined errors since computed would be looking for data not yet loaded for the components. Props would be missing, so I scrapped that idea in the end. I could not hide the component until scrolled into view, since if it was hidden (the plugin you pointed out which was exactly what I wanted, thanks), would not know its in view since its hidden. Maybe I scrapped it to fast and need to trouble shoot more ideas with it.

This is the basic shell of the 15+ component modules. These are what are on the index page of the dashboard. All load a very similar if not exact code base, all load a {name}Table.vue as default content to show.
ScheduleMgt.vue code (pulling data from Vuex, no API call)

<script>
import { mapState, mapActions } from 'vuex'
export default {
  name: 'ScheduleMgt',
  components: {
    ScheduleTable: () =>
      import(
        /* webpackChunkName: "dashboard" */ '~/components/dashboard/schedule/ScheduleTable'
      ),
    ScheduleForm: () =>
      import(
        /* webpackChunkName: "dashboard" */ '~/components/dashboard/schedule/ScheduleForm'
      ),
    SwitchComponentBtns: () =>
      import(
        /* webpackChunkName: "dashboard" */ '~/components/ui/switchComponentBtns'
      )
  },
  computed: {
    ...mapState({
      count: state => state.scheduleStore.count,
      records: state => state.scheduleStore.records,
      loading: state => state.scheduleStore.loading,
      currentComponent: state => state.scheduleStore.component
    }),
    endPoint() {
      return 'schedule/' + this.$route.params.slug
    }
  },
  mounted() {
    // each component is re-usable and can be used on its own individual page also
    if (this.$route.name !== 'dashboard') {
      const payload = {
        url: this.endPoint,
        method: 'get',
        store: 'scheduleStore'
      }
      this.callServer(payload)
    }
  },
}
</script>

<template>
  <v-card :elevation="6" color="white">
    <z-toolbar title="Schedule" divider>
      <template slot="side"
        ><v-spacer />
        <v-chip
          disabled
          small
          color="blue-grey lighten-5 fw-6 blue-grey--text text--darken-2"
          ><strong>{{ count }}</strong></v-chip
        >

        <switch-component-btns
          component-store="scheduleStore"
          :current-component="currentComponent"
          :component-options="['ScheduleTable', 'ScheduleForm']"
        />
      </template>
    </z-toolbar>

    <v-card-text class="z-flex-auto pa-0">
      <vue-element-loading :active="loading" />

      <!-- START DYNAMIC COMPONENT -->
      <keep-alive>
        <transition name="expand-slow" mode="out-in">
          <component :is="currentComponent"></component>
        </transition>
      </keep-alive>
      <!-- END DYNAMIC COMPONENT -->
    </v-card-text>

    <div class="z-card-footer">
      <v-icon color="blue-grey lighten-3">update</v-icon>
      <p>
        Dates not set to repeat will automatically be removed once date has
        passed.
      </p>
    </div>

  </v-card>
</template>

And on the dashboard, the page with issues are simply placed
index.vue snip: (there are 15 so this 1/5 rows basically)

<v-layout row wrap>
  <v-flex xs12 lg4>
    <service-areas-mgt />
  </v-flex>
  <v-flex xs12 lg4>
    <schedule-mgt />
  </v-flex>
  <v-flex xs12 lg4>
    <contact-mgt />
  </v-flex>
</v-layout>

I will keep trying different ideas, combination of everything I have tried but just looking for ideas since I seem to be out of ideas to try at this point.

Thanks again for your time.

I notice you’re using async components. This is likely why the loading seemed to take longer - as, I assume, the components didn’t actually load until they came into view… so the browser has to fetch the component, the data, and then render it. Something to consider.

This is how I’d approach it, a much more simplified example of course.

https://jsfiddle.net/jamesbrndwgn/ubf368mt/1/

2 Likes

Thanks for the code example.

That’s pretty much the exact goal I am looking to accomplish.
Going to do some re-coding of the data fetching and overhaul a few components to try it out.

Will post results / issues / questions.

Much appreciated!

1 Like

Great jsfiddle @JamesThomson, I had no idea about the IntersectionObserver API. Shame about lack of IE support.

In your example you’ve create a new IntersectionObserver instance for every target. Do you know if there is any performance hit of doing this, if you had lots of targets? Having a single observer instance with multiple targets would make the code more complicated, since it can only have one callback.

To answer my own question, interesting discussion here: https://github.com/w3c/IntersectionObserver/issues/81

Seems like it’s OK to have many observers with one-observer-per-target. Makes things easier.

Can’t say I’ve noticed any performance issues. I use IntersectionObserver to lazy load images, load new data (endless scrolling), etc. So at any given time a page may have a few dozen active observers.

OK what I did based on your example was(not sure if this is correct)
Only tested on 3 of my dashboard widgets

1.) I made an observe component from your example
2.) Inside each dashboard component say ScheduleMgt i wrapped the whole thing inside

<observer :once="true" :threshold="0" @enterView="getData">
..original ScheduleMgt code
</observer>

Imported Observer and added the observation mounted, beforeDestroy, and methods to the ScheduleMgt component, as per your example (will make a mixin since its all repeated) if this is correct implementation?

But inspecting the Elements the html is still rendered. How can I make it so it loads the html on demand when coming into view?

I added
inView: false
in data of the component, then in my getData() made
this.inView = true

And used a v-if="inView" on the

<component v-if="inView" :is="currentComponent"></component>

So far its looking promising!

Thanks

Final results.

Dashboard above the folder loads in less than a second. Only pull in ~10KB of data in the single API call for the above the fold components, so all and all I would say its a success …BUT…except for 1 thing.

Always a but.

This is a Nuxt setup, and prior to adding the observer, every nav / page change resulted in the new page being positioned at the top of the page (what I wanted).
Now it keeps previous page position. So if I scroll half way on page “a”, navigate to page “b” and page “b” loads at page “a” position, not at the top where it should / used to be.

I will make a post for that issue since its off topic of this.

That should be just a matter of updating Vue-Router’s scrollBehaviour. https://nuxtjs.org/api/configuration-router/#scrollbehavior

// scrolls to the top on every route change
export default function (to, from, savedPosition) {
  return { x: 0, y: 0 }
}

In my routes.js I have:

const scrollBehavior = (to, from, savedPosition) => {
  // savedPosition is only available for popstate navigations.
  if (savedPosition) {
    return savedPosition
  } else {
    let position = {}
    // if no children detected
    if (to.matched.length < 2) {
      // scroll to the top of the page
      position = { x: 0, y: 0 }
    } else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
      // if one of the children has scrollToTop option set to true
      position = { x: 0, y: 0 }
    }
    // if link has anchor, scroll to anchor by returning the selector
    if (to.hash) {
      position = { selector: to.hash }
    }
    return position
  }
}

And thats been site the dev on this site started months back and has not changed, i only noticed the page not at x:0,y:0 when i started going back and forth testing the observer yesterday. So will have to sort that out. Will drip in your snip and try since my snip uses and checks for stuff I know I am not using / hash and such.

Thanks again for chiming in with assistance :slight_smile:

I did just notice something acting weird upon further testing. (Testing on Chrome / FF/ Safari / Opera browsers)

Usual UX path for user is login and that directs to dashboard.
No problems, F5 / hard refresh on dashboard no issues.
Navigate to another link, hit F5 / refresh on that page and then navigate back to dashboard I get errors:

Displays on error page.
Failed to execute ‘observe’ on ‘IntersectionObserver’: parameter 1 is not of type ‘Element’.

Full Console Message:
TypeError: Failed to execute ‘observe’ on ‘IntersectionObserver’: parameter 1 is not of type ‘Element’.
at VueComponent.mounted (app.js:17957)
at invokeWithErrorHandling (commons.app.js:13991)
at callHook (commons.app.js:16345)
at mountComponent (commons.app.js:16169)
at VueComponent.push…/node_modules/vue/dist/vue.runtime.esm.js.Vue.$mount (commons.app.js:20537)
at init (commons.app.js:15253)
at createComponent (commons.app.js:18100)
at createElm (commons.app.js:18047)
at createChildren (commons.app.js:18175)
at createElm (commons.app.js:18076)

So not sure why that happens.
Tried changing mounted to beforeMount to no success.