primers

When we do the performance optimization of the Web end, we basically involve the loading and parsing process of HTML, in which how to control the loading and execution timing of Javascript is particularly important.

There are many Javascript loading methods involved here, including synchronous, asynchronous, and dynamic addition. Then, when will they start loading and when will they start executing? Here is a brief summary. Of course, if you’re too lazy to watch the process, you can just slide to the end and see the summary

Note that this article is based on Chrome version 94 and does not represent all browser behavior

Demo

Practice is the only standard to test the truth, just like hearing a lot of truth, relying on bad life, or the whole Demo out intuitive experience, may be different from the imagination. Here is a simple use of node.js most basic functions to write a Demo, code in script-load (github.com/waiter/Scri…

Yarn start or NPM run start can be used to start a simple Node server. You can then visit http://localhost:3000 in your browser to see the various examples

In addition, in order to simulate the loading time of the script more conveniently, and to clarify the loading sequence and execution time, all Javascript is directly typed into Node.js, and Node.js waits for a period of time before returning. The code involved is relatively simple, and the general process is as follows

const wait = (time: number) = > new Promise(resolve= > {
  setTimeout(resolve, time);
});

const makeScript = (jsName: string, needWait: number = 0) = > {
  const waitStr = needWait > 0 ? ` wait: ${needWait}` : ' ';
  return `console.log('${jsName}${waitStr}'); `;
}

const server = http.createServer(async (req, res) => {
  / /... Some logic
  if (reqPath.endsWith('.js')) {
    const needWait = (query.wait && +query.wait) || 0;
    if (needWait > 0) {
      await wait(needWait);
    }
    res.write(makeScript(jsName, needWait));
  } else {
    // Other logic
  }
  res.end();
});
Copy the code

So that the front end can directly use something like /7.head-child.js? Wait =1000 to load JS that takes 1s.

In addition, there are two important things that happen when we look at Web optimization:

  • DOMContentLoaded: HTML parsed
  • Load: Resources are loaded

I also added a listener for both events to all the sample HTML to see when it was executed

document.addEventListener('DOMContentLoaded'.(event) = > {
  console.log('DOMContentLoaded');
});
window.addEventListener('load'.(event) = > {
  console.log('loaded');
});
Copy the code

Synchronous loading

First, a brief explanation of the concept of synchronous loading is that it blocks the continued parsing of HTML. We know that the HTML parsing process is parsed from top to bottom, but when a script tag needs to be loaded synchronously, we will wait for the script to complete loading and execution, and then continue parsing the HTML.

Here is a simple example to see if it meets expectations:

<script src="/1.normal1.js? wait=1000"></script>
<script src="/2.normal2.js? wait=500"></script>
<script src="/3.normal3.js? wait=500"></script>
Copy the code

The result is also simple:

1.normal1.js wait: 1000
2.normal2.js wait: 500
3.normal3.js wait: 500
DOMContentLoaded
loaded
Copy the code

Execute in order, and execute before DOMContentLoaded.

You’ll notice that all three scripts load at exactly the same time, immediately after the HTML is retrieved. This involves an optimization of the browser, which finds all script tags in the HTML in advance and starts loading them directly, rather than when the tag is parsed. This is a browser optimization, such as HTML parsing to the corresponding tag, JS has been loaded, directly execute, this can save a lot of time, improve user experience. Of course, from the above execution results, its execution timing or maintain the original logic.

async

The Script tag also supports async and defer properties for asynchronous loading, that is, does not block parsing of the HTML.

For async, start by referring to an MDN introduction

For classic scripts, if the async attribute is present, then the classic script will be fetched in parallel to parsing and evaluated as soon as it is available.

For module scripts, if the async attribute is present then the scripts and all their dependencies will be executed in the defer queue, therefore they will get fetched in parallel to parsing and evaluated as soon as they are available.

This attribute allows the elimination of parser-blocking JavaScript where the browser would have to load and evaluate scripts before continuing to parse. defer has a similar effect in this case.

For what we’re going to look at now, it’s simply asynchronous loading, executed as soon as it’s loaded.

So here’s a quick example

<script src="/1.async1.js? wait=3000" async></script>
<script src="/2.normal1.js? wait=500"></script>
<script src="/3.async2.js? wait=100" async></script>
<script src="/4.normal2.js? wait=400"></script>
Copy the code

As a result,

2.normal1.js wait: 500
4.normal2.js wait: 400
DOMContentLoaded
3.async2.js wait: 100
1.async1.js wait: 3000
loaded
Copy the code

3.async2.js is executed before 4.normal2.js. What’s the loading time?

Normal1.js, 3. Async2.js and 4. Normal2.js are already loaded after the 2.normal1.js execution is complete, which shows that synchronous script execution takes precedence over asynchronous execution. What if the loading time of 4.normal2.js is extended

<script src="/1.async1.js? wait=3000" async></script>
<script src="/2.normal1.js? wait=500"></script>
<script src="/3.async2.js? wait=100" async></script>
<script src="/4.normal2.js? wait=600"></script>
Copy the code

The results were as expected

2.normal1.js wait: 500
3.async2.js wait: 100
4.normal2.js wait: 600
DOMContentLoaded
1.async1.js wait: 3000
loaded
Copy the code

defer

The script tag also supports defer for asynchronous loading

This Boolean attribute is set to indicate to a browser that the script is meant to be executed after the document has been parsed, but before firing DOMContentLoaded.

Scripts with the defer attribute will prevent the DOMContentLoaded event from firing until the script has loaded and finished evaluating.

Scripts with the defer attribute will execute in the order in which they appear in the document.

This attribute allows the elimination of parser-blocking JavaScript where the browser would have to load and evaluate scripts before continuing to parse. async has a similar effect in this case.

It basically loads asynchronously, waiting until the HTML is parsed, but in order before the DOMContentLoaded event

So let’s write a simple example

<script src="/1.defer1.js?wait=3000" defer></script>
<script src="/2.normal1.js? wait=500"></script>
<script src="/3.defer2.js?wait=100" defer></script>
<script src="/4.normal2.js? wait=400"></script>
Copy the code

The execution result is

2.normal1.js wait: 500
4.normal2.js wait: 400
1.defer1.js wait: 3000
3.defer2.js wait: 100
DOMContentLoaded
loaded
Copy the code

As expected, the JS loading timing is optimized by the browser to load all of them at first.

Let’s combine async

<script src="/1.normal1.js? wait=400"></script>
<script src="/2.defer1.js?wait=200" defer></script>
<script src="/3.async1.js? wait=600" async></script>
<script src="/4.defer2.js?wait=700" defer></script>
<script src="/5.normal2.js? wait=500"></script>
Copy the code

The execution result is

1.normal1.js wait: 400
5.normal2.js wait: 500
2.defer1.js wait: 200
3.async1.js wait: 600
4.defer2.js wait: 700
DOMContentLoaded
loaded
Copy the code

It’s normal. There’s nothing to talk about

createElement

Another common way to dynamically load a script is to use createElement to dynamically create a script tag and then add it to the HTML. For easy testing, I’ve added a simple method to the HTML

window.loadScript = function(url, async) {
  var po = document.createElement('script');
  po.async = async;
  po.src = url;
  document.body.appendChild(po);
};
Copy the code

The async property defaults to true, which is passed in for debugging purposes. Now that you have the creation method, let’s try another example here

<script>loadScript('/1.create1.js? wait=3000'.true);</script>
<script src="/2.normal1.js? wait=500"></script>
<script>loadScript('/3.create2.js? wait=100'.true);</script>
<script src="/4.normal2.js? wait=700"></script>
Copy the code

Let’s look at the results

2.normal1.js wait: 500
3.create2.js wait: 100
4.normal2.js wait: 700
DOMContentLoaded
1.create1.js wait: 3000
loaded
Copy the code

It is similar to writing async script tags, but it is more intuitive to load because it cannot be optimized by the browser, as shown below

The async property of the create script tag has the same name as the async property of the normal script tag. If the property is set to false, the async property can be used to dynamically create synchronous scripts. Have a try

<script>loadScript('/1.create1.js? wait=3000'.false);</script>
<script src="/2.normal1.js? wait=500"></script>
<script>loadScript('/3.create2.js? wait=100'.false);</script>
<script src="/4.normal2.js? wait=700"></script>
<script>
  setTimeout(function() {
    loadScript('/5.create3.js? wait=3000'.false);
  }, 1000);
</script>
Copy the code

And then what happens?

2.normal1.js wait: 500
4.normal2.js wait: 700
DOMContentLoaded
1.create1.js wait: 3000
3.create2.js wait: 100
5.create3.js wait: 3000
loaded
Copy the code

DOMContentLoaded does not block HTML parsing. Then look for the documentation

In older browsers that don’t support the async attribute, parser-inserted scripts block the parser; script-inserted scripts execute asynchronously in IE and WebKit, but synchronously in Opera and pre-4 Firefox. In Firefox 4, the async DOM property defaults to true for script-created scripts, so the default behavior matches the behavior of IE and WebKit.

To request script-inserted external scripts be executed in the insertion order in browsers where the document.createElement("script").async evaluates to true (such as Firefox 4), set async="false" on the scripts you want to maintain order.

In other words, async=”false” only allows create script tags to be executed sequentially and asynchronously

In addition, the above example provides another way to illustrate that the HTMLonload event will not fire until the previously loaded resource has finished loading

document.write

In addition to the above method of loading JS, there is also a less commonly used document.write, which has not been used much before, but will be examined together.

window.writeScript = function(url, type) {
  document.write('<scr' + 'ipt '+ type +' src="'+ url + '"></scr' + 'ipt>');
};
Copy the code

Add a create method, notice that script is deliberately broken apart, mainly to prevent the browser from parsing this as a script tag.

In addition, also added a blocking JS process method, to achieve the effect of delay

window.waitTime = function(time) {
  var start = new Date().getTime();
  while (new Date().getTime() - start < time) {}
  console.log('wait: ' + time);
};
Copy the code

When you’re ready, start testing

<script src="/1.normal1.js? wait=1000"></script>
<script>
  writeScript('/2.write-normal1.js? wait=500'.' ');
  waitTime(600);
  writeScript('/3.write-normal2.js? wait=500'.' ');
  console.log('write end');
</script>
<script src="/4.normal3.js? wait=500"></script>
Copy the code

Look at the results

1.normal1.js wait: 1000
wait: 600
write end
2.write-normal1.js wait: 500
3.write-normal2.js wait: 500
4.normal3.js wait: 500
DOMContentLoaded
loaded
Copy the code

Because document.write is used to write ordinary script tags, so it also shows the synchronization characteristics of ordinary Script tags, blocking the browser to continue parsing, but does not block the current script tag code execution. Write end is printed before 2.write-normal1.js

Also, since this is a dynamically created script tag, the browser can’t optimize it

Document. write in addition to writing ordinary script tags, of course, can also write async script tags

<script>writeScript('/1.write-async1.js? wait=3000'.'async');</script>
<script src="/2.normal1.js? wait=500"></script>
<script>writeScript('/3.write-async2.js? wait=100'.'async');</script>
<script src="/4.normal2.js? wait=400"></script>
Copy the code

The execution result is

2.normal1.js wait: 500
4.normal2.js wait: 400
DOMContentLoaded
3.write-async2.js wait: 100
1.write-async1.js wait: 3000
loaded
Copy the code

There is also the script tag with defer

<script>writeScript('/1.write-defer1.js? wait=3000'.'defer');</script>
<script src="/2.normal1.js? wait=500"></script>
<script>writeScript('/3.write-defer2.js? wait=100'.'defer');</script>
<script src="/4.normal2.js? wait=400"></script>
Copy the code

The execution result is

2.normal1.js wait: 500
4.normal2.js wait: 400
1.write-defer1.js wait: 3000
3.write-defer2.js wait: 100
DOMContentLoaded
loaded
Copy the code

This looks like document.write should be handy when you don’t want to use browser optimizations, but you want to block HTML parsing and so on.

What are its limitations? Look at an example

<script src="/1.normal1.js? wait=200&url=%2F2.write1.js%3Fwait%3D100"></script>
<script src="/3.async1.js? wait=200&url=%2F4.write2.js%3Fwait%3D100" async></script>
<script src="/5.defer1.js?wait=200&url=%2F6.write3.js%3Fwait%3D100" defer></script>
<script>loadScript('/7.create1.js? wait=100&url=%2F8.write4.js%3Fwait%3D100'.false);</script>
<script>
  setTimeout(function() {
    writeScript('/9.normal2.js? wait=200'.' ');
  }, 300);
</script>
Copy the code

The JS request above adds a URL parameter to return the JS with document.write. The logic in Node is

const makeScript = (jsName: string, needWait: number = 0, url? : string, type? : string) = > {
  const waitStr = needWait > 0 ? ` wait: ${needWait}` : ' ';
  let script = `console.log('${jsName}${waitStr}'); `;
  if (url) {
    script += `document.write('<script ${type || ' '} src="${url}"></script>'); `;
  }
  return script;
}
Copy the code

What is the result of the above example?

The core limitations of this warning are:

  • Cannot be used in asynchronous scriptsIt didn’t work, but it didwarning
  • Should not be used after DOMContentLoaded, you will find that your HTML is completely overwritten

A small test

Now that you’ve read all the above, take a simple quiz to see if you understand the content

<script src="/1.normal1.js? wait=500"></script>
<script src="/2.async1.js? wait=600" async></script>
<script src="/3.defer1.js?wait=500" defer></script>
<script>loadScript('/4.create1.js? wait=4000'.false);</script>
<script src="/5.defer2.js?wait=1000&url=%2F6.write1.js%3Fwait%3D1000" defer></script>
<script>loadScript('/7.create2.js? wait=1000'.false);</script>
<script>writeScript('/8.write-async1.js? wait=200'.'async');</script>
Copy the code

What is the result of the above execution? The answer is below

👉 Please click on this line to see the answer 👈




If the image is not shown above, you can click: p1-juejin.byteimg.com/tos-cn-i-k3…

conclusion

Loading form Optimized browser loading synchronous The orderly Available after HTML parsing
ordinaryScriptThe label — –
ordinaryScriptLabel async — –
ordinaryScriptLabel the defer — –
createElement
createElementAnd async is false
document.writeordinary
document.writeOrdinary async
document.writeOrdinary defer

PS: code in script-load (github.com/waiter/Scri…