preface

Yesterday, when browsing the RSS feed, I saw an article “My View on Svelte” posted by Qizu Weekly. I saw a code example that uses native JavaScript to implement a Counter that does not use Virtual DOM:

const target = document.querySelector('#app');

// state
let count = 0;

// view
const div = document.createElement('div');
const countText = document.createTextNode(`${count}`);
div.appendChild(countText);

const button1 = document.createElement('button');
const button1Text = document.createTextNode(` + `);
button1.appendChild(button1Text);

const button2 = document.createElement('button');
const button2Text = document.createTextNode(` - `);
button2.appendChild(button2Text);

target.appendChild(div);
target.appendChild(button1);
target.appendChild(button2);

// event
button1.addEventListener('click'.() = > {
  count += 1;
});
button2.addEventListener('click'.() = > {
  count -= 1;
});
Copy the code

The example used document.createTextNode to insert the associated DOM Text. I was curious about this method because I had never used this method to create textNodes and insert them as child nodes into the associated DOM.

Normally, I would use innerText to insert a Text into the relevant DOM, which creates a problem:

  • When writing code, what is the use ofdocument.createTextNodeCreate a TextNode and insert it into the relevant DOM, again directlyinnerTextIs it more efficient to insert Text?

Watch “Hello FE” for more

Train of thought

To answer this question, the first thought was to run two separate pieces of test code using Jest and then look at the time difference between the two.

But if you think about it, Jest runs in Node, not in a browser, so there are no DOM objects and you have to simulate them using JSDOM, so you’re running pure JavaScript. Without JavaScript modifying DOM objects through Web IDL, there is no way to measure the difference between the two approaches.

Why is front-end DOM manipulation the most performance intensive? – Answer by JustJavac – Zhihu

But, since it’s a test, it wouldn’t hurt to run around and see if, as I think, you can’t detect the difference between a non-DOM operation and a DOM operation.

JSDOM

document.title & document.xxx

Let’s start with two pieces of code to verify that we really can’t detect the difference between non-DOM and DOM operations in the Node environment.

const {JSDOM} = require('jsdom');

const {window} = new JSDOM(` `);

const {document} = window;

console.time();
for (let i = 0; i <= 1e4; i++) {
  document.title = i;
}
console.timeEnd();
Copy the code
const {JSDOM} = require('jsdom');

const {window} = new JSDOM(` `);

const {document} = window;

console.time();
for (let i = 0; i <= 1e4; i++) {
  document.xxx = i;
}
console.timeEnd();
Copy the code

And it turns out that it’s not what we thought it was:

$node jsdom/dom.js default: 195.461msCopy the code
$node jsdom/non-dom default: 0.281msCopy the code

The question is, why is there such a big difference even in the Node environment?

Debug the document. Title change that triggers the setter:

Making changes to document. XXX is just an assignment:

The difference is that the Document Object wraps part of its properties with Object.defineProperty, where title overrides its getter/setter, so modifying Document.title takes significantly more time.

Document. title = document. XXX = document. XXX

Now go back to testing the difference between document.CreateTextNode and innerText.

createTextNode & innerText

const {JSDOM} = require('jsdom');

const {window} = new JSDOM(` 
      
`
); const {document} = window; const root = document.getElementById('root'); console.time(); for (let i = 0; i <= 1e4; i++) { const text = document.createTextNode(i); root.append(text); } console.timeEnd(); Copy the code
const {JSDOM} = require('jsdom');

const {window} = new JSDOM(` 
      
`
); const {document} = window; const root = document.getElementById('root'); console.time(); for (let i = 0; i <= 1e4; i++) { root.innerText += i; } console.timeEnd(); Copy the code

The results were surprising:

$node jsdom/createTextNode Default: 103.349msCopy the code
$ node jsdom/innerText
default: 0.459ms
Copy the code

Very unexpected! I did another round of debugging and found that the HTMLElement returned by the getElementById method provided by JSDOM didn’t have innerText.

Changing the innerText becomes a simple assignment, as in the case of modifying Document. XXX above.

Chrome

Version 1

Since I couldn’t do this test in a Node environment, I switched to Chrome to see what the difference would be if I ran it in Chrome.

So I created two pages to test:

<div id="root"></div>
<script>
  const root = document.getElementById('root');

  console.time();
  for (let i = 0; i <= 1e4; i++) {
    const text = document.createTextNode(i);
    root.appendChild(text);
  }
  console.timeEnd();
</script>
Copy the code
<div id="root"></div>
<script>
  const root = document.getElementById('root');

  console.time();
  for (let i = 0; i <= 1e4; i++) {
    root.innerText += i;
  }
  console.timeEnd();
</script>
Copy the code

The results were surprisingly wide:

/ / the default: 3.133056640625 ms
Copy the code
/ / the default: 11603.7099609375 ms
Copy the code

In this case, createTextNode performs much better than modifying innerText.

There shouldn’t be such a big gap! What’s the problem?

Going back to the JavaScript code and comparing, we see that we modify innerText to perform the same behavior as createTextNode with a += operation.

Is it too time-consuming here?

After thinking about it, we decided to redesign the experiment to remove the += operation while ensuring the consistency of the final results.

Version 2

<div id="root"></div>
<script>
  const root = document.getElementById('root');

  console.time();
  for (let i = 0; i <= 1e4; i++) {
    const child = document.createElement('div');
    const text = document.createTextNode(i);
    child.appendChild(text);
    root.appendChild(child);
  }
  console.timeEnd();
</script>
Copy the code
<div id="root"></div>
<script>
  const root = document.getElementById('root');

  console.time();
  for (let i = 0; i <= 1e4; i++) {
    const child = document.createElement('div');
    child.innerText = i;
    root.appendChild(child);
  }
  console.timeEnd();
</script>
Copy the code

After the experiment was improved, the behavior of the two experiments should be strictly controlled, and the += operation was removed. Now the results are very close:

/ / the default: 6.649169921875 ms
Copy the code
/ / the default: 6.679931640625 ms
Copy the code

To make sure this result is accurate, let’s magnify the number of cycles by an order of magnitude and test again.

When the number of cycles is magnified by an order of magnitude, the result looks like this:

/ / the default: 54.6650390625 ms
Copy the code
/ / the default: 54.66015625 ms
Copy the code

It’s still very close, almost the same.

Is it clear that there is no difference in performance between these two different ways of writing?

In fact, there may be problems with the improved experiment. What’s the problem?

Version 3

The problem lies in the code order in the body of the loop:

const root = document.getElementById('root');

console.time();
for (let i = 0; i <= 1e5; i++) {
  const child = document.createElement('div');
  const text = document.createTextNode(i);
  child.appendChild(text);
  root.appendChild(child);
}
console.timeEnd();
Copy the code
const root = document.getElementById('root');

console.time();
for (let i = 0; i <= 1e5; i++) {
  const child = document.createElement('div');
  child.innerText = i;
  root.appendChild(child);
}
console.timeEnd();
Copy the code

Why is there a problem with the code order in the body of the loop?

Because a child is still a JavaScript object until it is inserted into root, changes to JavaScript object properties can be made very quickly.

Insert child into root first, then insert Text.

const root = document.getElementById('root');

console.time();
for (let i = 0; i <= 1e5; i++) {
  const child = document.createElement('div');
  root.appendChild(child);
  const text = document.createTextNode(i);
  child.appendChild(text);
}
console.timeEnd();
Copy the code
const root = document.getElementById('root');

console.time();
for (let i = 0; i <= 1e5; i++) {
  const child = document.createElement('div');
  root.appendChild(child);
  child.innerText = i;
}
console.timeEnd();
Copy the code

And the results:

/ / the default: 57.946044921875 ms
Copy the code
/ / the default: 56.62109375 ms
Copy the code

At this point we should be able to determine that there is no performance difference between the two methods of inserting Text.

conclusion

Because a technical article reading leads to so some content, and then tested, also not sure test method is right, if there is a big guy with understanding can give advice ~