浏览器从获取 HTML 到最终在屏幕上显示,需要完成 关键渲染路径 如下:
-
处理 HTML 标记并构建 DOM 树;
-
处理 CSS 标记并构建 CSSOM 树;
-
将 DOM 和 CSSOM 合并成一个 render tree;
-
根据渲染树布局,计算每个节点的几何信息;
-
将各个节点绘制到屏幕;
我们想要优化渲染内容,就要最大限度的缩短以上步骤耗费的总时间,可以关注三个方面:
-
关键资源的数量:减少不必要的加载;
-
关键路径的长度:注意较大文件的阻塞加载;
-
关键字节的大小:压缩文件;
优化DOM
-
缩小文件的尺寸(Minify);
-
开启gzip压缩(Compress);
-
使用缓存(HTTP Cache);
优化CSSOM
CSS的加载会阻塞关键渲染路径(也会阻塞JS的加载),对于首页不可见的元素样式,我们可以采用异步加载的方式去防止阻塞:
-
打印样式:
media="print" -
拆分媒体查询:
media="(min-width: 1000px)" -
设备方向:
media="orientation: portrait" -
内联样式:比如loading;
-
避免使用
@import;
优化JS加载
JS的加载也会阻塞关键渲染路径,一般可采用异步方式来优化:
script标签的属性
-
defer:异步加载,会在DOM解析完毕之后、DOMContentLoaded事件之前执行,有序执行; -
async:异步加载,加载完立即执行;
link标签的属性
-
rel="preload":预加载,利用空闲时间加载指定资源,可在文档底部引入使用; -
rel="prefetch":预加载,适用于非当前页面的js文件加载;
优化动画
-
优先使用 CSS 3 动画,CSS 3 动画是通过GUI解析的,不会阻塞主线程,使用 3D 属性还能开启GPU渲染;
-
使用
window.requestAnimationFrame替代 setInterval 动画,requestAnimationFrame 是根据浏览器刷新频率来决定回调时机的,可以防止丢帧造成的卡顿现象;const run = () => { if(false){ return } ... // 执行内容 window.requestAnimationFrame(run); } window.requestAnimationFrame(run);
WebWorker多线程
我们可以将一些纯计算的工作迁移到WebWorker上处理,待其处理完毕后,再将结果返回给主线程,这样可以使主线程更专注于处理UI交互。
在使用中需要注意以下几点:
-
DOM限制:Woker无法读取主线程中的DOM对象,只能访问navigator和location对象;
-
文件读取限制:Worker无法访问本地文件系统,这就要求所加载的脚本来自网络;
-
通信限制:主线程和Worker子线程不在同一个上下文中,无法直接通信,只能通过消息来完成;
-
脚本执行限制:可通过XMLHTTPRequest发起Ajax请求,但不能使用alert、confirm方法;
-
同源限制:Worker子线程执行的代码文件必须与主线程的代码文件同源;
使用示例:
// 创建Worker子线程
const worker = new Worker("worker.js");
// 点击时执行
btn.addEventListener("click", () => {
worker.postMessage({
type: "add",
data: {
num1,
num2
}
});
});
// 监听子线程消息事件
worker.addEventListener("message", (e) => {
const {type, data} = e.data;
if(type === "add"){
// 展示结果
result.contentText = data;
}
});
// worker.js
// 监听来自主线程的消息事件
onmessage = function(e){
const {type, data} = e.data;
if(type === "add"){
const sum = data.num1 + data.num2;
// 给主线程发布事件
postMessage({type: "add", data: sum});
}
}
在子线程处理完相关任务后,需要及时关闭以节省系统资源,方式有两种:
-
主线程:
worker.terminate(); -
子线程:
self.close();
其他优化
防抖与节流
页面滚动、窗口大小改变、input内容变化、按钮点击 等交互场景,如果需要监听回调,可根据业务需求选择防抖或节流
计算样式优化
CSS引擎在查找样式表时,对每条规则的匹配顺序是从右到左的,为了提高页面的渲染性能,计算阶段应尽量减少参与的元素数量:
-
使用类选择器代替标签选择器:
.product-list_li代替.product-list li -
避免使用通配符做选择器:
html, body, ... {}代替* {} -
降低选择器的复杂性,避免考虑不周导致的样式问题;
-
使用BEM书写规范:块(Block)、元素(Element)、修饰符(Modifier)
-
中划线:作为多个单词中间的连字符;
-
单下划线:描述状态;
-
双下划线:连接块与块的子元素;
-
type-block__element_modifier
// 常规写法: .mylist {} .mylist .item {} .mylist .item .small {} .mylist .item .big {} .mylist .item .size10 {} // BEM写法 .mylist__item {} .mylist__item_small {} .mylist__item_big {} .mylist__item_size-10 {} -
重绘和回流
页面的绘制会带来大量的性能开销,我们应从代码层面出发,尽量避免页面的layout、尽量最小化layout的次数。
首先我们分析会引起页面布局与重绘的操作:
-
对DOM元素几何属性的修改:width、height、padding、margin、left、top 等;
-
对DOM结构的更改:增、删、移动;
-
获取某些需要即时计算的属性:offsetWidth、offsetHeight、offsetTop、scrollTop、... 等;
那我们如何避免样式的频繁改动呢?可注意以下几点:
-
使用类名来进行样式的统一修改,避免js逐条修改;
-
可使用变量缓存对属性值的计算,避免多次设置样式;
-
使用requestAnimationFrame控制渲染帧:跟其特性有关,回调函数中多次取值即时属性,其实取到的是上一帧的值,并不会触发页面布局的重新计算;