前端渲染效率优化

896 阅读9分钟

1. 概述

经历了网络请求过程,客户端从服务器获取到了所访问的页面文件之后,浏览器便要开始渲染服务器响应回来的内容。

首先浏览器会通过解析HTMLCSS文件来构建DOMCSSOM

浏览器接收读取到HTML文件,其实是根据文件指定编码的原始字节,首先需要将字节转换为字符串,再将字符串转换为W3C标准规定的令牌结构,令牌就是HTML中不同标签代表不同含义的一组规则结构。然后经过词法分析将令牌转化为定义了属性和规则值的对象,最后将这些标签根据HTML表示的父子关系,连接成树形结构。

DOM树表示文档标记的属性和关系,但未包含其中各元素经过渲染后的外观呈现,这便是接下来CSSOM的职责了,与将HTML文件解析为文档对象模型的过程类似,CSS文件也会首先经历从字节到字符串,然后令牌化及词法分析后构建为层叠样式表对象模型。

这两个对象模型的构建过程是会花费时间的,可以通过浏览器的开发者工具性能选项卡查看到对应过程的耗时情况。

得到文档对象模型和层叠样式表对象之后就要进行绘制,在呈现之前,浏览器需要将文档对象模型和样式模型合并到一起最终形成一颗渲染树。这棵树中只包含可见的节点,比如displaynode的节点就是不包含的。

从所生成的DOM树的根节点开始向下遍历每个子节点,忽略所有不可见的节点,因为不可见的节点不会出现在渲染树中。

CSSOM中为每个可见的子节点找到对应的规则并应用。

布局节点根据所得到的渲染树,计算他们在试图设备中的具体位置和大小,这一步输出的是一个盒模型。接着绘制节点将每个节点的具体绘制方案转化为屏幕上的实际像素。

构建渲染树,布局,及绘制过程所需要的时间取决于实际文档的大小。文档过大,浏览器需要处理的任务就越多样式也复杂,绘制需要的时间就越长。所以关键渲染路径执行快慢,将直接影响首屏加载时间的性能指标。

当首屏渲染完成后,用户在和网站的交互过程中,有可能通过JavaScript代码提供的操作接口更改渲染树的结构。一旦DOM结构发生改变,这个渲染过程就会重新执行一遍。

2. DOM优化

HTML文件的尺寸应该尽可能的小,目的是为了让客户端尽可能早的接收到完整的HTML,通常HTML中有很多冗余的字符,例如注释,空行,换行,废弃代码。对于生产环境的HTML来说应该删除一切无用的代码,尽可能保证HTML文件精简。

3. CSSOM

首次构建网页时,js常常受阻于css,确保将任何非必须的css都标记为非关键资源,并应确保尽可能减少关键css的数量,以及尽可能缩短传输时间。

4. JS

所有文本资源都应该尽可能的小,js也需要删除未使用的代码,缩小文件体积,使用缓存,避免同步请求,异步加载js,延迟解析,避免使用运行时间长的js

5. requestAnimationFrame

window.requestAnimationFramesetInteral方法相比最大的优势是将回调函数执行时机交由系统来决定,如果屏幕刷新率是60Hz,则他的回调函数大约16.7ms执行一次,如果是75Hz,则13.3ms执行一次。也就是说requestAnimationFrame方法的执行时机与系统的刷新率同步。

这样能保证回调函数在屏幕的每次刷新间各种只被执行一次,从而避免因随机丢帧而造成的卡顿现象。

6. WebWorker

众所周知JavaScript是单线程执行的,所有任务放在一个线程上执行,只有当前一个任务执行完才能处理后一个任务,不然后面的任务只能等待,这就限制了多核计算机充分发挥它的计算能力。同时在浏览器上,JavaScript的执行通常位于主线程,这恰好与样式计算,页面布局及绘制一起,如果JavaScript运行时间过长,必然会导致其他工作任务的阻塞而造成卡顿。

可以将一些纯计算的工作迁移到Web Worker上处理,他为JavaScript的执行提供了多线程环境,主线程通过创建出Worker子线程,可以分担一部分自己的任务执行压力,在Worker子线程上执行的任务不会干扰主线程,待其上的任务执行完成后,会把结果返回给主线程,这样的好处是让主线程可以更专注的处理UI交互,保证页面的使用体验流程。

需要注意的是一旦创建Worker成功便会一直执行,不会被主线程上的事件所打断,这就意味着Worker会比较耗费资源,所以不应当过度使用,一旦任务执行完毕就应该及时关闭。

Worker无法进行DOM操作,也无法读取本地文件,主线程和子线程只能通过消息机制通信,不能直接通信。子线程和主线程要求同源,不能跨域。可以使用Ajax但不能使用alertconfirm

// 创建Worker线程
const worker = new Worker('work.js')
// 主线程中关闭
worker.terminate();

// 子线程中关闭
self.close();

7. 节流

function throttle(fn,delay) {
    let lastTime;
    let timer;
    delay || (delay = 300)
    rteurn function (arguments) {
        let context = this;
        let args = arguments;
        let nowTime = +new Date();
        if (lastTime && nowTime < lastTime + delay) {
            clearTimeout(timer);
            timer = setTimeout(function() { // 到达执行时间
                lastTime = nowTime;
                fn.apply(context,args);
            },delay)
        } else { // 当前距离上次执行的时间大于等于设置时间,立即执行
            lastTime = nowTime;
            fn.apply(context,args)
        }
    }
}

8. 防抖

function debounce(fun,delay) {
    return function (arguments) {
        let context = this;
        let args = arguments;
        clearTimeout(fun.id)
        fun.id = setTimeout(function() {
            fun.call(context,args)
        },delay)
    }
}

9. 计算样式优化

JavaScript处理后若发生了添加和删除元素,对样式属性和类进行了修改,就都会导致浏览器重新计算所涉及的元素样式。某些修改还可能会引起页面布局的更改和浏览器的重新绘制。

CSS引擎在查找样式表时,对每条规则的匹配顺序是从右向左的。这与我们通常从左向右书写习惯相反。

.class li {}

首先选择器会找到页面中所有的li,然后再去查询li的祖先级找到.class用于缩小范围,所以是从大范围缩小到小范围的。

建议使用类选择器代替标签选择器,这样就减少了从整个页面中查找标签元素的范围,毕竟在css选择器中,标签选择器的区分度是最低的。

避免使用通配符做选择器*,这种操作在规模较小的demo项目中几乎看不到任何性能差异,但对实际的工程项目来说使用通配符就意味着在计算样式时浏览器需要去遍历页面中的每一个元素,这样的性能开销很大。

开发中也应该尽量降低选择器的复杂性,最好使用单一类名的样式名称还要尽量避免重名。

BEM是一种CSS的书写规范,他的名称由于BlcokElementModifier三个单词组成,分别代表块,元素和修饰符。

理论上他希望每行CSS代码只有一个选择器,这就是为了降低选择器的复杂性。对选择器的命名要求通过以下三个符号的组合来实现。

中划线-仅作为连字符使用,表示某个快或子元素的多个单词之间的连接符

单下滑线_作为描述一个块或其子元素的一种状态

双下划线__作为连接块与块的元素

.type-block__element_modifier

10. 页面布局重绘

页面的布局也叫做重排和回流,指的是浏览器对页面元素的几何属性进行计算并将最终结果绘制出来的过程。

凡是元素的宽高尺寸,在页面中的位置及隐藏或显示等信息发生改变时,都会触发页面的重新布局。

通常页面布局的作用范围会涉及整个文档,所以这个环节会带来大量的性能开销,我们在开发过程中,应当从代码层面出发,尽量避免页面布局或最小化其处理次数,如果仅修改了DOM元素的样式,而未影响其集合属性时,则浏览器会跳过页面布局的计算环节,直接进入重绘阶段。

虽然重绘的性能开销不及页面布局高,但未了更高的性能体验,也应当降低重绘发生的频率和复杂度。

要避免重绘首先就是对DOM元素几何属性的修改控制,当修改了widthheightpaddingmarginlefttop等属性便会波及与他相关的所有节点元素进行几何属性的重新计算。这也很好理解元素大小位置变了,重新计算也是应该的。

其次是更改了DOM树结构,增删了DOM节点,不过这里只会影响后面的元素,前面的元素不受影响。

最后是获取某些特定的属性值操作,比如可见区域宽高,offsetWidth,offsetHeight,页面视窗中元素与视窗边界的距离,offsetTopoffsetLeft,类似的属性值还有scrollTop,scrollLeftscrollWidthscrollHeightclientTopclientWidthclientHeight以及调用了window.getComputedStyle方法。 这些属性都是需要通过计算得到的,所有浏览器需要重新进行布局计算获取相应的值。

尽量避免页面布局的变化,一些动画可以采用CSS动画来替代js动画。

js中如果需要修改样式,尽量使用类名将样式操作合并,最后统一替换className的方式修改,不要逐条修改样式。

// bad
div.style.height = '100px';
div.style.width = '100px';

// good

div.classList.add('classname');

计算属性值如果需要使用最好js中做缓存,不要每次使用都重新获取,触发数据发生变化。

尽可能将多次DOM操作合并成一次,比如向页面中添加10div,最好的做法是将10div拼接到一起之后统一添加到页面,不要添加10次。