前端性能优化(二) - 渲染优化

191 阅读4分钟

浏览器从获取 HTML 到最终在屏幕上显示,需要完成 关键渲染路径 如下:

  1. 处理 HTML 标记并构建 DOM 树;

  2. 处理 CSS 标记并构建 CSSOM 树;

  3. 将 DOM 和 CSSOM 合并成一个 render tree;

  4. 根据渲染树布局,计算每个节点的几何信息;

  5. 将各个节点绘制到屏幕;

我们想要优化渲染内容,就要最大限度的缩短以上步骤耗费的总时间,可以关注三个方面:

  1. 关键资源的数量:减少不必要的加载;

  2. 关键路径的长度:注意较大文件的阻塞加载;

  3. 关键字节的大小:压缩文件;

优化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控制渲染帧:跟其特性有关,回调函数中多次取值即时属性,其实取到的是上一帧的值,并不会触发页面布局的重新计算;