本篇文档主要总结脚本和样式在不同位置加载时浏览器 HTML 解析的一些情况以及最佳实践经验
脚本是否阻塞 HTML 解析 如下代码,在 head
标签内部外链两段脚本,然后使用 Chrome 浏览器 Performance 进行分析,发现浏览器在执行到第 5 行的时候就会暂停解析 HTML 直到脚本下载完毕并执行完毕后再继续解析,其中脚本下载花费12.77ms
,图中两个阶段为蓝色,由此可见 JS 的脚本会阻塞 HTML 解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 脚本阻塞HTML渲染</title > <script src ="https://cdn.jsdelivr.net/npm/vue@3.3.6/dist/vue.global.min.js" > </script > <script src ="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js" > </script > </head > <body > <h1 > hello world</h1 > </body > </html >
仔细观察执行结果后发现脚本下载的时间比解析 HTML 稍早,这是为什么呢?
这是因为浏览器的渲染引擎有一个预解析的线程,在接受到 HTML 数据之后,预解析线程会快速扫描 HTML 数据中的关键资源,一旦扫描到了,会立马发出请求。——李兵《浏览器工作原理与实践》
还有一个奇怪的现象,脚本在下载完成后只执行了 vue.global.min.js 就继续从第 6 行开始解析 HTML 了,随后才执行 react.production.min.js ,这个又如何解释呢?
如果使用 defer 或者 async 来加载脚本是否有不同? 1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 脚本阻塞HTML渲染</title > <script async src ="https://cdn.jsdelivr.net/npm/vue@3.3.6/dist/vue.global.min.js" > </script > </head > <body > <h1 > hello world</h1 > </body > </html >
使用 async 脚本加载情形:
使用 defer 加载脚本情形: 发现async 加载的脚本不阻塞 HTML 的解析了,脚本下载完成后会立即执行,从图中看到 DCL
事件被提前了很多。但在使用 defer 加载脚本不会立即执行,会在 DCL
事件前执行。
使用 preload 提前预加载脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > 脚本阻塞HTML渲染</title > <link rel ="preload" href ="https://cdn.jsdelivr.net/npm/vue@3.3.6/dist/vue.global.min.js" as ="script" > <link rel ="preload" href ="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js" as ="script" > </head > <body > <h1 > hello world</h1 > <script src ="https://cdn.jsdelivr.net/npm/vue@3.3.6/dist/vue.global.min.js" > </script > <script src ="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js" > </script > </body > </html >
由此可以看到两个资源几乎同时加载了,由于在 body 结束前我们又加载了脚本此时不会重复加载脚本,脚本被立即执行了。如果我们提前加载了脚本又不使用的话在 Chrome 浏览器会发出如下警告:
动态加载脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Script Add</title > </head > <body > <h1 > hello world</h1 > <script > function addScript (src ) { const script = document .createElement ('script' ); script.type = 'text/javascript' ; script.src = src; document .querySelector ('head' ).appendChild (script); } window .onload = function ( ) { addScript ('https://cdn.jsdelivr.net/npm/vue@3.3.6/dist/vue.global.min.js' ); addScript ('https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js' ); } </script > </body > </html >
由图可以看出使用 JS 动态创建脚本的方式也是不阻塞 HTML 解析的,而且加载完后也立即执行了。
样式是否会阻塞 HTML 渲染 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Style script</title > <link rel ="stylesheet" href ="https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.min.css" > <script > for (let i = 0 ; i <= 1000 ; i++) { console .log (i); } </script > </head > <body > <h1 > hello world</h1 > </body > </html >
如上代码当通过外链的方式引入 CSS 时后面又紧跟脚本,脚本会等待样式加载完后再执行,所以需要尽量避免这种情况
最佳的脚本加载方式
关键资源使用 preload 进行提前加载
非关键资源在闲时使用动态加载的方式