浏览器的多线程中, 有的线程负责加载资源, 有的线程负责执行脚本, 有的线程负责渲染界面, 有的线程负责轮询, 监听用户事件.
这些线程, 根据浏览器自身特点以及 web 标准等等, 有的会被浏览器特意的阻塞. 两个很明显的阻塞就是: 脚本执行时对其他线程的阻塞和脚本加载时对其他线程的阻塞.
这两个阻塞发生在 html 页面初次解析时, 它们对性能的影响较大, 原因是:
document 对象绑定了一个事件: DOMContentLoaded. 这个事件会在 DOM 解析完成之后触发. 这个事件触发之后 (而不是 window.load 事件), 会进入异步事件驱动阶段(另一个线程控制). 也就是说, DOM 解析工作不完成, 用户与页面的很多(并不是所有) 事件交互就无法进行. 这时候浏览器的忙指示 (那个页面上方的烦人的旋转的圆圈) 不会消失.
DOMContentLoaded 什么时候触发?
DOMContentLoaded 事件本身不会等待 CSS 文件, 图片, iframe 加载完成.
DOMContentLoaded 的触发时机是: 加载完页面, 解析完所有标签(不包括执行 CSS 和 JS), 但是 JS 的执行, 需要等待位于它前面的 CSS 加载(如果是外联的话), 执行完成, 因为 JS 可能会依赖位于它前面的 CSS 计算出来的样式. 所以:
如果页面中没有 script 标签, DOMContentLoaded 事件并没有等待 CSS 文件, 图片加载完成.
如果页面中静态的写有 script 标签, DOMContentLoaded 事件需要等待 JS 执行完才触发. 而且 script 标签中的 JS 需要等待位于其前面的 CSS 的加载完成.
注: 现代浏览器会并发的预加载 CSS,JS,IMG(例如: 当 HTML 解析器 (HTML Parser) 被脚本阻塞时, 解析器虽然会停止构建 DOM, 但仍会识别该脚本后面的资源, 并进行预加载). 但是, 执行 CSS 和 JS 的顺序还是按原来的依赖顺序(JS 的执行要等待位于其前面的 CSS 和 JS 加载, 执行完)-- 先加载完成的资源, 如果其依赖还没加载, 执行完, 就只能等着.
所以就造成外部资源阻塞渲染, 如 CSS 与 JavaScript
默认情况下, CSS 被视为阻塞渲染的资源, 这意味着浏览器将不会渲染任何已处理的内容, 直至 CSSOM 构建完毕.
JavaScript 不仅可以读取和修改 DOM 属性, 还可以读取和修改 CSSOM 属性.
默认情况下, CSS 被视为阻塞渲染的资源, 存在阻塞的 CSS 资源时, 浏览器会延迟 JavaScript 的执行和 DOM 构建, 这意味着浏览器将不会渲染任何已处理的内容, 直至 CSSOM 构建完毕.
总结如下:
css 加载不会阻塞 DOM 树的解析
css 加载会阻塞 DOM 树的渲染
css 加载会阻塞后面 js 语句的执行
css 会阻塞 js, 同理, css 也会阻塞 img 解码, paint(浏览器认为你的 CSS 没有加载完毕, 不确定图片的样式到底如何, 牵扯到重绘资源问题),js 不会阻塞 img 的解码, paint(估计 chrome 做了优化, 具体本人还不知, 希望客官补充).
css 阻塞优化:
还可以用媒体类型 (media type) 和媒体查询 (media query) 来解除对渲染的阻塞.
media="print", 会加载, 但不会阻塞; media="(min-width:320px)", 会在符合查询条件下阻塞(适配 css 会执行)
大 css 文件拆分成多个小 css 文件, 并发加载
因为渲染线程和 js 线程与资源进行加载的线程并不互斥, 不会互斥意味着: 资源的加载可以和 UI 渲染, 重排, 事件响应, 或者 JavaScript 代码的执行的并发进行.
所以资源加载器线程会一直进行并发加载.
这里还有一个知识点: 下载的最大并行数指的是从一个主机上下载的最大并行数, 如果从多个主机下载资源, 这个数量会翻倍, 但是由于对 DNS 的解析也是一个性能优化的点, 故而一般策略是: 不应设置超过 4 个主机, 最好只设置 2 个主机.
但是操蛋的就是, 如果浏览器解析 DOM 时需要下载脚本资源, 那么下载这个资源的线程就是阻塞其他下载线程以及渲染线程, 导致渲染速度变慢.
但是假设该脚本下载的速度较慢, 而且多个脚本非并发下载, 并且假如多个内脚本执行时间较长的话, DOM 解析工作还是会一直完不成.
故而我们需要无阻塞加载脚本的技术.
js 阻塞优化
因为: 脚本执行和渲染 DOM 的并发可能会引发严重的冲突(脚本可以修改 DOM)
所以: JavaScript 引擎和渲染引擎所在的两个线程被设计为互斥的!
这就意味着: 在执行中内容时, 浏览器会切换到 JavaScript 引擎所在的线程, 此时渲染引擎所在的线程会阻塞, 故其后元素的解析和渲染会暂停. 这时候如果脚本执行时间太长的话, 不仅后面的元素会一直看不到, 对 DOM 的解析工作也会一直完不成. 用户会陷入焦急的等待中.
为了防止 javascript 阻塞, 我们会
1, 把放到紧跟之前的位置
这样就不会影响需要放到页面上的 UI 元素的解析了. 这样的好处就是, 用户能即使看到页面上的 UI 元素, 而防止出现了浏览器白屏等现象.
2, 动态脚本元素 - 不重要的 js 动态插入.
因为 document.createElement("script")的 async 属性默认为 true, 而 document.head.appendChild 代码之后, 由于没有触发渲染树的重绘, 切换回的渲染线程会将剩下的 DOM 解析并渲染完毕. 同时新插入的中的资源也会并发的下载.
- var script=document.createElement("script");
- console.log(script.async);//true
同理: 用 XHR 对象下载代码, 并注入到页面也可以达到同样的效果
如果需要同步执行, 需要将 async 属性设置为 fasle
3,h5 时代, script 添加 defer 或 asyn 两个属性(html4.0 中定义了 defer;html5.0 中定义了 async)
如果 script 标签中包含 defer, 那么这一块脚本将不会影响 HTML 文档的解析, 而是等到 HTML 解析完成后才会执行. 而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发. 即: 整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关), 会执行所有由 defer-script 加载的 JavaScript 代码, 然后触发 DOMContentLoaded 事件. defer 不会改变 script 中代码执行顺序
如果 script 标签中包含 async, 则 HTML 文档构建不受影响, 不需要等待 async-script 执行. 但是, async-script 加载完成后, 就会立即执行! 如果页面还是没有解析完成, 就会停下来 (阻塞页面) 等此脚本执行完毕再继续解析. async-script 可能在 DOMContentLoaded 触发之前或之后执行, 但一定在 load 触发之前执行. 而且: 多个 async-script 的执行顺序是不确定的.
document.readyState
说道 DOMContentLoaded, 不得不提 readystatechange, 通过 document.readyState 值来更进一步来判断文档状态:
uninitiated:xml 对象被产生, 但没有任何文件被加载.
loading:document 正在下载, 文件尚未开始解析.
loaded: 部分的文件已经加载且进行解析, 但对象模型尚未生效.
interactive:document 完成了解析, 但是资源还在下载, 对象模型是有效但只读的.
complete: 代表加载成功, 文档加载完成, 并且所有 resource 都加载完毕
通过下面代码验证, 在 chrome 上貌似只有 interactive 和 complete.
- document.addEventListener("DOMContentLoaded",function () {
- console.log("DOMContentLoaded"+new Date())
- });
- document.addEventListener("readystatechange",function () {
- console.log("B_____"+new Date());
- console.log(document.readyState)
- // switch (document.readyState){
- // case "loading":
- // console.log("LOADING"+new Date());
- // break;
- // case "loaded":
- // console.log("loaded"+new Date());
- // break;
- // case "interactive":
- // console.log("interactive"+new Date());
- // break;
- // case "complete":
- // console.log("complete"+new Date());
- // break;
- // }
- });
- console.time("A")
- A: 5.89208984375ms
B_____Thu May 17 2018 10:23:36 GMT+0800 (CST)
- interactive
- DOMContentLoadedThu May 17 2018 10:23:36 GMT+0800 (CST)
B_____Thu May 17 2018 10:23:36 GMT+0800 (CST)
complete
但是, 今天看了: 你不知道的 DOMContentLoaded https://zhuanlan.zhihu.com/p/25876048
这里又有疑问: interactive DOMContentLoaded complete onload 三个先后顺序是什么呢?
DOMContentLoaded 和 interactive: 表示文档解析完成, 且资源未完全加载完成. 区别呢? 执行顺序呢?
验证表明: interactive DOMContentLoaded complete onload
但是, DOMContentLoaded 触发时候, document.readyState 一般是 interactive, 也有可能 complete. 而当页面有大量的二进制文件(页面加载的时长大于阻塞的时长的时候),document.readyState=complete 可能反而在 onload 事件之后才能触发(这个我未完成验证出这种情况)
我觉得 onreadystatechange 这个不是很靠谱, 一般用 DOMContentLoaded 判断页面解析完全. 希望哪位大牛提供这方面的补充, 感激不尽!
在图片上, 也有 onload 跟 complete
- document.getElementById('load').onclick = function() {
- var img = new Image();
- if(img.complete) {
- console.log('dd');
- }
- img.onload = function() {
- console.log('ff')
- }
- img.src="images/1-logo.png";
- }
这里顺带提下 img 加载相关 属性
onload: 表示加载好, 换言之, 没有加载好不会执行;
onAbort: 图片加载的时候, 用户通过点击停止加载时出发
onerror: 如果图片不存在(网络很不通畅, 也可能触发 onerror 事件)
complete: 图片显示出来以后为 true,
来源: http://www.jianshu.com/p/be6e00cd64fb