How to optimize rendering of blaze.js templates on pages with lots of elements

Hello,

I’m new to Meteorjs. I am planning out a project that is a web app that does mental health assessments, and then generates a report with charts, accordion sections, tables, and other information. I’m concerned that using blaze.js I may run into issues with slow rendering times, or server bottlenecks. I’m not expecting huge traffic on the site because it something that users are likely to use once or twice a month. I am more concerned about client-side rendering. If the entire page(s) with every element open is going to be rendered at once; I’m concerned that it will lead to jank or some other problems. I see a few options. One option is to render the elements as they are needed. Either as the elements come into focus, or if it’s an accordion, when the accordion is opened. If this is possible, this would be my preferred approach. The other approach is to user spinners or a modal that would give the user feedback as the rest of the elements loaded. What is possible, and what is the best approach? Would React.js offer better options?

1 Like

How many elements are we talking?
For most applications, you won’t notice any difference in rendering performance unless you are frequently changing data and forcing frequent re-renders.

I find the slowest part is the initial loading (downloading, parsing, compiling and initial run of JS files) and then a very complex application will run speedily.

2 Likes

It varies, but it could really be a lot. The issue isn’t the data. The data is static, and it is just a list of scores and maybe 8 text questions with under 40 characters for each text field.

The issue is the UI rendering, and how it is done. If it is rendering out in a lazy, or ‘just in time’ fashion, then it’s ok. If not I’ll need to implement something like spinners or modals to avoid UI problems.

If the data is static, then it shouldn’t matter. The browser will happily render 10k elements in less than 1s on average hardware.

AFAIK, there is no framework that does lazy rendering of off-screen content out of the box.
It’s possible to do with most of them, to different degrees, by splitting your template into chunks with observers, but the processing overhead of that is likely to be far worse than just rendering it out once and being done.

The most time intensive part of browser rendering is layout calculation, which occurs when adding, or removing elements in normal flow, or changing content in a way that changes the width or height of an element.
If everything is rendered all at once, the layout calculation only needs to happen once

Since your data is static, I would really not worry too much. Try it out and profile the performance, fix issues after they appear

1 Like

Thank you, that answers my question. This application has a lot of static content that is rendered out dynamically. Depending on which disorders a person qaulifies for there assessment outcomes, and their ‘action plan’ (which is a pdf they get at the end to send their doctors), has lots of content. This content needs to be stored in a localization table. That data also has to be coming from the server. I probably should’ve mentioned that. Is there a way to eager load items in the localization tables?

Thanks for addressing this topic. I was looking for the information regarding the same.

If have implemented a nice little template to render lazy lists as proof of concept. It uses the IntersectionObserver api and I would like to show you how it works.

Consider you have some list of data, in this case some array with thousand entries, where each entry is 0,1,2,3,…,999:

main.js

Template.body.onCreated(function () {
  let allElements = []
  allElements.length = 1000
  allElements.fill(0)
  allElements = allElements.map((val, index) => index)

  this.allElements = new ReactiveVar(allElements)
})

Now you want to render this “lazy”, say in 50 element steps, depending on how far the end of the current list is close to be visible on the viewport. You pass this data to a template, let’s say lazyList:

main.html

 {{#lazyList data=allElements step=50}}
   <h1>Entry {{this}}</h1> {{!-- "this" refers to the entry at the current index --}}
 {{/lazyList}}

Getting the allElements from a simple helper:

main.js

Template.body.helpers({
  allElements () {
    return Template.instance().allElements.get()
  }
})

So our list should look like this in the end:

Entry 0

Entry 1

Entry 2

Entry 3

Entry 4

Entry 5

Entry 6

Up to 50 elements.

In order to achieve that our lazyList tempate needs to render the list of elements and pass to the content block the current element:

lazyList.html

<template name="lazyList">
    {{#each element in elements}}
        <div class="lazy-list-element" data-index="{{@index}}">
            {{> Template.contentBlock element }} {{!-- pass the data back to the h1 html content --}}
        </div>
    {{/each}}
    {{#if loadComplete}}
        <div class="lazy-list-end"></div>
    {{/if}}
</template>

At the end of the list is en empty div with class name lazy-list-end. It will be our indicator to render the next 50 elements. The following code contains all important hints as comments:

lazyList.js

import { ReactiveDict } from 'meteor/reactive-dict'
import './lazyList.html'

Template.lazyList.onCreated(function () {
  const instance = this
  instance.state = new ReactiveDict()
  instance.state.set('length', instance.data.step)


  instance.autorun(() => {
    // we fill a shallow copy of the array up to the max length
    // and use it as data source for our list rendering
    // this part will automactically re-activate when the
    // length value has been extended in our handleIntersect function
    const currentStep = instance.state.get('length')
    const elements = []
    elements.length = currentStep

    // copy elements / references
    for (let i = 0; i < currentStep; i++) {
      elements[i] = instance.data.data[i]
    }

    // we use loadComplete to delay the rendering
    // of the end element, so it won't appear before
    // we have copied our list
    instance.state.set({ elements, loadComplete: true })
  })
})

Template.lazyList.helpers({
  elements () {
    return Template.instance().state.get('elements')
  },
  loadComplete () {
    return Template.instance().state.get('loadComplete')
  }
})

Template.lazyList.onRendered(function () {
  const instance = this
  const options = {
    root: null,
    rootMargin: '0px',
    threshold: 1.0
  }


  const observer = new IntersectionObserver(function handleIntersect (entries, observer) {
    entries.forEach((entry) => {
      // once the end element intersects with the viewport
      // we can extend the current length by the value of step
      // which is on our case 50
      if (entry.isIntersecting) {
        const { step } = instance.data
        const length = instance.state.get('length')
        instance.state.set('length', length + step)
      }
    })
  }, options)
  observer.observe(document.querySelector('.lazy-list-end'))
})

Now it should add 50 more elements each time you reach the end of the list. Please let me know if there are any issues with the code.

If you like to see this in a package in atmosphere please leave a :heart: I think this has the potential for a generic lazy-render package :slight_smile:

Edit: I forgot to mention, that this is just an example for a fixed size list but can easily be changed for lists, where the next data is fetched from the sever on end.

4 Likes