加载、执行与渲染
关键渲染路径 Critical rendering path
CRP是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。
- 浏览器请求指定的 HTML 文件 ,服务器返回 指定的HTML文件。
- 浏览器按HTML文件中的代码顺序进行解析,依次转换为 DOM 树。
- 当浏览器发现有外部资源(如外联的样式、脚本或者图片、字体等资源,就会再发起一个HTTP请求。
- 如果是阻塞类型的资源,意味着浏览器将停下来等待,直到该资源被载入和处理完毕,才会解析后面的 HTML 。
- 浏览器接着解析 HTML,发送请求和构造 DOM 直到文件结尾,这时开始构造 CSS 对象模型。
- 等到 DOM 和 CSSOM 完成之后,浏览器构造渲染树,计算所有可见内容的样式。
- 一旦渲染树完成布局开始,定义所有渲染树元素的位置和大小。
- 完成之后,页面被渲染完成,或者说是绘制到屏幕上。
浏览器的解析过程
- 构建DOM树
- 预加载扫描器:优化相关资源的加载
- 构建CSSOM树
- Javascript编译
- 被解析、编译和解释。被解析为抽象语法树;有些浏览器引擎会进一步编译为字节码;然后在主线程上解释执行(也有例外,例如在 web worker 中运行的代码)
- 构建无障碍树:构建对应于无障碍对象模型(AOM) 的无障碍树。
用户体验与阻塞 blocking
考虑如下场景:
- html文档中全体资源总尺寸过大,加载时间太长,将导致白屏,即用户迟迟看不到任何有吸引力的资源
- html文档中包含大量装饰性图片。暂时不显示这些图片不会显著影响内容的呈现
- html文档中包含大量图片,但这些图片大多不在第一屏的可见视窗之内。
从用户角度:
- 阻塞式体验:用户需要等待html文件上所有的资源都被加载和执行完成后才能得到渲染树,即看到最终的页面。
- 非阻塞式体验:用户首先看到关键内容(如html文本和基本样式),再逐步看到非关键内容(美化的样式、各类图片等)
等待页面加载的时间越长,用户在页面加载完成之前离开的概率就越大.
渲染阻塞 render blocking
HTML文件中存在不同类型的资源,如html元素、CSS样式、JS代码、图片、Web字体等,并且从渲染的角度,它们之间存在着相互等待或依赖的关系。
- CSSOM的构建必须等待相应的HTML元素被加载和构建。
- 由于级联的特性,多段CSS代码之间也存在依赖关系,即必须等待所有样式加载后,才能得到最终的样式树。
- JavaScript中如果包含DOM读取操作,则必须等待相应的HTML元素被加载和构建;反过来,JavaScript中如果包含DOM更新操作,则最终的DOM树必须等待JavaScript被加载和执行。
- 多段JavaScript代码之间同样可能存在依赖关系,比如函数调用顺序,因此,调用方(caller)同样需要等待被调方(callee)被加载和执行。
资源的的加载顺序决定了页面的正确性,但同时制约了对渲染顺序的优化。
一个简单的优化方法是:将所有js、css代码置于页面底部
网络阻塞 IO blocking
大部分情况下,浏览器是单线程执行的。主线程负责html文件的解析和渲染。 由于CSS样式、JS代码、图片、Web字体等资源还存在外联的载入方式。当主线程在html文件中解析到外联资源时,有两种情况:
- 在主线程中发起新的HTTP请求,并且等载资源加载完成后,再继续后续的解析。称之阻塞式操作。
- 开启一个新的线程,采用异步方式发起新的HTTP请求,并且不等待响应就继续后续文档的解析,称之非阻塞式操作。
- 见后续关于异步的讨论
由于网络连接的不可靠性,阻塞式网络IO极易出现卡死等现象;而另一方面,非阻塞式IO可以快速渲染主文档或关键内容,改善用户体验。但使得解析顺序变得不可预测,需要处理好资源之间的依赖性。
预加载扫描器
浏览器一般内置有预加载扫描器,它用来优化各类资源的加载,减少阻塞操作,即不必等到解析器遇到外部资源的引用后再对其进行请求和加载。它将在后台预先检索HTML文件中的各类资源,当主 HTML 解析器遇到相应资源时,这些资源可能已经被下载或执行。
<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="图像描述" />
<script src="anotherscript.js" async></script>
本例中,图片和脚本都会被预先加载
从渲染阻塞的角度, CSS 加载不会阻塞 HTML 的加载,但是它确实会阻塞 JavaScript,因为 JavaScript 经常需要查询元素的 CSS 属性。
脚本
阻塞模式
<script src="app.js" ></script>
阻塞主线程,立即加载、立即执行预加载模式
<script src="app.js" async></script>
通知预加载扫描器,该脚本可预加载(异步方式),但其执行时点仍然是<script>
在html文件中的代码位置延迟执行模式
<script src="app.js" defer></script>
预加载该脚本,并且在整个html文件完成解析后再执行。前端开发者习惯于将脚本放置在<head>
中,但这要么会拖慢页面加载,要么会报dom元素不存在的错误,从而不得不放置在页面底端。defer属性可以很好的解决这个问题。
async、defer等属性仅限外联资源
图片
<img>
默认是阻塞式网络操作。显示一幅高精度图片会使得用户持久等待。可编写脚本实现延迟加载或懒加载:
//本脚本置于页面最底端或window.onload中
let imagesToLoad = document.querySelectorAll("img[data-src]");
const loadImages = (image) => {
image.setAttribute("src", image.getAttribute("data-src"));
image.onload = () => {
image.removeAttribute("data-src");
};
};
为了避免图片延迟加载导致的突然撑大文本,可以给每个图片预加载一个轻量级的占位图片。
其它方案还有:按需加载、渐进式加载等
CSS
默认情况下,CSS 被视为渲染阻塞资源,因此,在 CSSOM 构造完成之前,浏览器不会渲染任何已处理的内容。 所以CSS 的尺寸应当尽量小,利用尽快送达,建议使用媒体类型和查询(media query)等实现非阻塞渲染。
字体
默认情况下,对字体的请求会延迟到构造渲染树之前,这可能会导致文本渲染被延迟。
可以使用
<link rel="preload">
、CSS font-display 属性和字体加载 API 来覆盖默认行为并预加载网络字体资源。
优化 CRP
提升页面加载速度需要通过被加载资源的优先级、控制它们加载的顺序和减小这些资源的体积。优化方法包括
- 通过异步、延迟加载或者消除非关键资源来减少关键资源的请求数量,
- 通过区分关键资源的优先级来优化被加载关键资源的顺序,来缩短关键路径长度。
- 优化必须的请求数量和每个请求的文件体积
- 比如css精灵图或定制的icon font
参考
- 渲染页面:浏览器的工作原理 https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work
- 关键渲染路径 https://developer.mozilla.org/zh-CN/docs/Web/Performance/Critical_rendering_path
- 懒加载 https://developer.mozilla.org/zh-CN/docs/Web/Performance/Lazy_loading
- 渐进式加载
- defer 和 async https://blog.csdn.net/VickyTsai/article/details/102841293
- 五种方法延迟加载图像,以获得更好的网站性能 https://zhuanlan.zhihu.com/p/176031132
- img元素的loading机制 https://www.cnblogs.com/moqiutao/p/7283129.html
- script元素 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script