glimmerjs / glimmer-vm

MIT License
1.13k stars 190 forks source link

Long inserts optimization #950

Closed lifeart closed 8 months ago

lifeart commented 5 years ago

@wycats @chancancode

Case: If we have long list, and append more nodes into it, rendering struggling.

Real life most used case:

let users = Array(20);

let moreUsers = Array(20);

let updatedUsers = [].concat(users, moreUsers);

How glimmer-vm doing it:

updatedUsers -> 1..19 - retain nodes, 20-40 -> insert nodes, this is 20 DOM API calls.

How to optimize?

- batch inserts while we see chain.

Example:

const insertStack = [];

node 21 -> if insert -> insertStack.push(createNode(21))
node 22 -> if insert -> insertStack.push(createNode(22))
node 23 -> if insert -> insertStack.push(createNode(23))
node 23 -> if (NOT insert) 
    -> entryForNode(21).appendChild(DocumentFragment(insertStack))
    -> do (NOT insert) case..
stefanpenner commented 5 years ago

Ah, i am surprise this is how we are doing it.

lifeart commented 5 years ago

https://ember-twiddle.com/edab79a0d0525d8dd8b8de1dbf82c527?openFiles=templates.components.performant-each.hbs%2Ctemplates.components.performant-each.hbs

image

Using fragments for initial loops rendering can bring 2x performance!

lifeart commented 5 years ago

https://ember-twiddle.com/7190940e60cf314aa2c26a708a884f23?openFiles=templates.application.hbs%2Ctemplates.components.performant-each.hbs - it's already working case (increase initial rendering performance 2x+), but, I don't think it support fastboot and add one more dom tag. No perf improvements on rerender

lifeart commented 5 years ago

Can't figure out how to get this optimization in glimmer-vm, but, we can create component, like

{{#super-each items=items fragmentSize=1000 as |item index|}}
<div> hello ! </div>
{{/super-each}}

inside super-each template:

<div {{ref this "container"}}>
  {{#let (chunk @items @fragmentSize) as |arraySlice|}}
    {{#each arraySlice as |arrayFragment sliceIndex|}}
      {{#let (create-fragment) as |fragment|}}
        {{#in-element fragment}}
          {{#each arrayFragment as |item index|}}
            {{yield item (add index (mult sliceIndex @fragmentSize))}}
          {{/each}}
        {{/in-element}}
        {{append-fragment fragment this.container}}
      {{/let}}
    {{/each}}
  {{/let}}
</div>
function appendFragment([fragment, node]) {
  node.appendChild(fragment);
  return '';
}
function createFragment([fragment, node]) {
  return document.createDocumentFragment();
}
function chunk(myArray, chunk_size) {
  let index = 0;
  const tempArray = [];
  const arrayLength = myArray.length;
  for (index = 0; index < arrayLength; index += chunk_size) {
    const myChunk = myArray.slice(index, index + chunk_size);
    tempArray.push(myChunk);
  }
  return tempArray;
}

tldr: we slice array into chunks and render each chunk as fragment

all we need - fix glimmer-vm error with "unknown parent" with fragment. code below already can be implemented (createDocumentFragment -> createElement('div')), but, we will get trash dom nodes between loops

lifeart commented 3 years ago

all optimizations could be applyed in https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/runtime/lib/vm/update.ts#L326

insertItem could be replaced with 'scheduleInsertItem' for "longAppend" case

image

image

lifeart commented 8 months ago

closing as implemented in https://github.com/lifeart/glimmer-next