前端性能优化

2023-05-31 0 566

前端性能优化

王钰

2016 年重新加入 Qunar,目前在去哪儿网互联网平台销售部后端构架组(YMFE)任后端技师一职。

热烈欢迎出访工程项目组网志YMFE 

前段时间工作中两个工程项目在运转时有许多操控性难题,有鉴于此我看了许多与操控性强化相关的文本,下面做个单纯的撷取。

后端操控性强化,这包括 CSS/JS 操控性强化、互联网操控性强化之类文本,这点的文本 《高操控性中文网站工程建设手册》《高操控性中文网站工程建设高阶指南》、《高操控性JavaScript》 之类书都做了许多传授,TNUMBERX写作。(那些引文参看责任编辑开头)

下面的文本,下面提及的书中大多包涵了,因此能考量急于去读那些书,做两个理所应当的介绍,对于责任编辑,也就不要再读下来了。

如果你秉持看到了这里,那就来聊聊我碰到的许多后端操控性难题,并聊聊软件系统。

1 优先选择强化对操控性负面影响大的部份

当应用领域有了操控性难题后,千万别硬生生扎到标识符中去,具体来说要再说那部份对操控性负面影响最大。优先选择强化那些对操控性负面影响大的部份,能起著坎氏见影的效用。

采用 Chrome DevTools ,能迅速地找出引致操控性转差的最主要不利因素,有关 Chrome DevTools 的采用TNUMBERX写作 Google Developers 下面的系列产品讲义 –Chrome DevTools

些该事件的反弹。这个这时候能采用console.count 来对继续执行单次进行统计数据。当这部份低频继续执行的标识符已经足够多强化的这时候,要是考量与否能增加继续执行单次。比如说两个时间维数为 O(n*n*n)的算法,再怎么强化也不如将其变为O(n*n) 来的快。

2 对低频触发的该事件进行节流或消抖

对于 Scroll 和 Touchmove 这类该事件,永远千万别低估了它们的继续执行频率,处理这类该事件的这时候能考量与否要给它们添加一个节流或者消抖过的反弹。节流和消抖,可能其他人不这么翻译,其实也就是debouncethrottle这两个函数。

debounce 和 throttle是两个相似(但不相同)的用于控制函数在某段该事件内的执行频率的技术。你能在 underscore 或者 lodash 中找出这两个函数。

2.1 采用 debounce 进行消抖

多次连续的调用,最终实际上只会调用一次。想象自己在电梯里面,门将要关上,这个这时候另外两个人来了,取消了关门的操作,过了一会儿门又要关上,又来了两个人,再次取消了关门的操作。电梯会一直延迟关门的操作,直到某段时间里没人再来。

所以debounce适合用在比如说对用户输入文本进行校验的这种场景下,多次触发只需要响应最后一次触发就好了。

2.2 采用 throttle 进行节流

将频繁调用的函数限定在两个给定的调用频率内。它保证某个函数频率再高,也只能在给定的该事件内调用一次。比如说在滚动的这时候要检查当前滚动的位置,来显示或隐藏回到顶部按钮,这个这时候能采用throttle 来将滚动反弹函数限定在每 300ms 继续执行一次。

需要提及的是,这两个函数常常被误用,且许多这时候当事人并没有意识到自己误用了。我曾经用错过,也见过别人用错。这两个函数都接受两个函数作为参数,然后返回两个节流/去抖后的函数,下面第二种用法才是正确的用法:

1234567// 错误的用法,每次该事件触发都得到两个新的函数$(window).on(scroll, function() {   _.throttle(doSomething, 300);});// 正确的用法,将节流后的函数作为反弹$(window).on(scroll, _.throttle(doSomething,200));

3 JavaScript 迅速,DOM 很慢

JavaScript 如今已经迅速了,真正慢的是 DOM。因此避免采用许多不易读但据说能提高速度的写法。不久前,

一位朋友对我说采用 ‘+’ 号将字符串转为数字比采用 parseInt快。对此我并没有怀疑,因为直觉上 parseInt 进行了函数调用,很可能会慢许多,我们一起在 node v6.3.0 上进行了许多验证,结果的确如我们所预计的那样,但是差别有多大呢,进行了 5 亿次迭代,采用+号的方法仅仅快了2秒。虽然快了两秒,但实际中将字符转为数字的操作可能只会进行几次,因此这样的做法根本没有意义,它只会让标识符变得更难读。12plus: 1694.392msparseInt: 3661.403ms

真正慢的是 DOM,DOM 对外提供了 API,而 JavaScript 能调用那些 API,它们两者就像是采用一座桥梁相连,每次过桥都要被收取大量费用,因此应该尽量让增加过桥的单次。

3.1 为什么 DOM 很慢

谈到这里需要对浏览器利用 HTML/CSS/JavaScript 等资源呈现出精彩的页面的过程进行单纯说明。浏览器在收到 HTML 文档之后会对文档进行解析开始构建 DOM (Document Object Model) 树,进而在文档中发现样式表,开始解析 CSS 来构建 CSSOM(CSS Object Model)树,这两者都构建完成后,开始构建渲染树。整个过程如下:

前端性能优化

渲染树的构建过程

在每次修改了 DOM 或者其样式之后都要进行 DOM树的构建,CSSOM 的重新计算,进而得到新的信息,浏览器会立刻进行一次重排。

3.2 避免强制性同步布局

在 JavaScript 中读取到的布局信息都是上一帧的信息,如果在 JavaScript 中修改了页面的布局,比如说给某个元素添加了两个类,然后再读取布局信息。这个这时候为了获得真实的布局信息,浏览器需要强制性对页面进行布局。因此应该避免这样做。

3.3 批量操作 DOM

在必须要进行频繁的 DOM 操作时,能采用 fastdom这样的工具,它的思路是将对页面的读取和改写放进队列,在页面重绘的这时候批量继续执行,先进行读取后改写。因为如果将读取与改写交织在一起可能引起多次页面的重排。而利用 fastdom 就能避免这样的情况发生。

虽然有了 fastdom 这样的工具,但有的这时候还是不能从根本上解决难题,比如说我前段时间碰到的两个情况,与页面单纯的一次交互(轻轻滚动页面)就继续执行了几千次 DOM 操作,这个这时候核心要解决的是增加 DOM 操作的单次。这个这时候要是从标识符层面考量,看看与否有不必要的读取。

另外许多有关高效操作 DOM 的方法,能参看《高操控性 JavaScript》相关章节,也能先参考一下我的读书笔记 《高操控性 JavaScript》(https://github.com/wy-ei/notebook/issues/34 )

4 强化渲染操控性

浏览器通常每秒更新页面 60 次,每一帧的时间就是 16.6ms,为了能让浏览器保持 60帧 的帧率,为了让动画看起来流畅,需要保证帧率达到 60fps,因此每一帧的逻辑需要在 16.6ms 内完成。

每一帧实际上都包涵下列步骤:

前端性能优化

因此,通常 JavaScript 的继续执行时间不能超过 10ms。

JavaScript:改变元素样式,添加元素到 DOM 中之类

Style:元素的类或者style改变了,这个这时候需要重新计算元素的样式

Layout:需要重新计算元素的具体尺寸

Paint:将元素的绘制的图层上

Composite:合并多个图层

当然也不是说每一帧都会进行那些操作。当你的 JavaScript 改变了某个 layout 属性,比如说元素的 width 和height 或者top之类,浏览器就会重新计算布局,并对整个页面进行重排。

如果修改了 backgroundcolor 这样的仅仅会让页面重绘的属性,这不会负面影响页面的布局,浏览器会跳过计算布局(layout)的过程,只进行重绘(paint)。

如果修改了两个不需要计算布局也不需要重绘的属性,那就只会进行图层的合并,这是代价最小的修改。从https://csstriggers.com/上你能知道修改那些样式属性会触发(Layout,Paint,Composite)中的那些操作。

4.1 将渐变或者会动画元素放到单独的绘制层中

绘制并非在两个单独的画布上进行的,而是多层。因此将那些会变动的元素提升至单独的图层,能让他的改变负面影响到的元素更少。

能采用 CSS 中的will-change: transform;或者 transform: translateZ(0); 这样来将元素提升至单独的图层中。

前端性能优化

采用 Chrome DevTools 来审查图层

在调试的这时候你能在 Chrome DevTools 的 timeline 面板来观察绘制图层。当然也不是说图层越多越好,因为新增加两个图层可能会耗费额外的内存。且新增加两个图层的目的是为了避免某个元素的变动负面影响其他元素。

4.2 降低绘制维数

某些属性的重绘相对而言更加复杂,比如说 filter、box-shadow 等滤镜或渐变效用。因此千万别滥用这类效用。

5 强化 JavaScript 的继续执行

下面提到的 JavaScript 强化,并不是说如何让 JavaScript 继续执行的更快,而是如何让 JavaScript 更高效地与 DOM 配合。

5.1 采用 requestAnimationFrame 来更新页面

我们希望在每一帧刚开始的这时候对页面进行更改,目前只有采用 requestAnimationFrame 能保证这一点。采用setTimeout 或者setInterval来触发更新页面的函数,该函数可能在一帧的中间或者结束的时间点上调用,进而引致该帧后面需要进行的事情没有完成,引发丢帧。

前端性能优化

采用 setTimeout 可能引致丢帧

requestAnimationFrame 会将任务安排在页面重绘之前,这保证动画能有足够多的时间来继续执行 JavaScript 。

5.2 采用 Web Worker 来处理复杂的计算

JavaScript 是在单线程的,并且可能会一直这样,因此 JavaScript 在继续执行复杂计算的这时候很可能会阻塞线程,引致页面假死。但 Web Worker 的出现,以另外一种方式给了我们多线程的能力,能将复杂计算放在 worker 中进行,当计算完成后,以postMessage的形式将结果传回来。

对于单个函数,因为 Web Worker 接受两个脚本的 url 作为参数,采用 URL.createObjectURL方法,我们能将两个函数的文本转换为 url,利用它创建两个 worker。

12345678910111213141516var workerContent = `self.onmessage = function(evt){    // …    // 在这里进行复杂计算var result = complexFunc();    // 将结果传回    self.postMessage(result);};`// 得到 urlvar blob = newBlob([workerContent]);var url = window.URL.createObjectURL(blob);// 创建 workervar worker = new Worker(url);

5.3 采用 transform 和 opacity 来完成动画

如今只有对这两个属性的修改不需要经历 layout 和 paint 过程。

6 强化 CSS

CSS 选择器在匹配的这时候是由右至左进行的,因此最后两个选择器常被称为关键选择器,因为最后两个选择越特殊,需要进行匹配的单次越少。要千万避免采用 *(通用选择器)作为关键选择器。因为它能匹配到所有元素,进而倒数第二个选择器还会和所有元素进行一次匹配。这引致效率很低下。

12/* 千万别这样做 */div p * {}

另外 first-child 这类伪类选择器也不够特殊,也要避免将它们作为关键选择器。关键选择器越特殊,浏览器就能用较少的匹配单次找出待匹配元素,选择器操控性也就越好。

还有两个老生常谈的注意事项,千万别采用太多的选择器。如果还有同学很悲剧地要兼容低版本 IE,要避免采用 CSS 表达式,它的操控性很差,详细文本可参看我之前记录的一篇笔记《高操控性中文网站工程建设手册》笔记(https://github.com/wy-ei/notebook/issues/15 )

7 合理处理脚本和样式表

如今有了 requirejs,webpack 等工具,可能很少会在页面中加载许多 JavaScript/CSS 标识符了。尽管如此,还是有必要聊聊如何合理处理脚本和样式表。

大多数人已经知道通常要把 JavaScript 放在文档底部,把 CSS 放在文档顶部。为什么呢?因为 JavaScript 会阻塞页面的解析,而外部样式表会阻塞页面的呈现和 JavaScript 的继续执行。

7.1 CSS阻塞渲染

通常情况下 CSS 被认为是阻塞渲染的资源,在CSSOM 构建完成之前,页面不会被渲染,放在顶部让样式表能尽早开始加载。但如果把引入样式表的 link 放在文档底部,页面虽然能立刻呈现出来,但是页面加载出来的这时候会是没有样式的,是混乱的。当后来样式表加载进来后,页面会立即进行重绘,这也就是通常所说的闪烁了。

7.2 JavaScript 阻塞文档解析

当在 HTML 文档中碰到 script 标签后控制权将交给 JavaScript,在 JavaScript 下载并继续执行完成之前,都不会解析 HTML。因此如果将 JavaScript 放在文档顶部,恰好这个这时候 JavaScript 脚本加载的特别慢,用户将会等待很长一段时间,这段个这时候 HTML 文档还没有解析到 body 部份,页面会是空白的。

另外常常被忽略的事实是:在浏览器没有下载并解析完成采用 link 引入的 CSS 文件之前,JavaScript 是不会继续执行的,因为 JavaScript 中可能需要读取样式,而此时样式表还没有加载回来,因此浏览器不会继续执行 JavaScript。能给 JavaScript 加上 async 标记,表示 JavaScript 的继续执行不会读取 DOM ,JavaScript 能不被 CSS 阻塞,能在空闲时间立刻继续执行。

综上所述,你更要保证 CSS 文件加载的足够多快。

有关这部份文本, 《高操控性中文网站工程建设手册》 上有很精彩的传授,墙裂推荐。《高操控性中文网站工程建设手册》我在读的这时候记录了笔记,能在这里看到。

最后TNUMBERX写作 Google Developers 中有关操控性强化的系列产品文章(https://developers.google.com/web/fundamentals/performance)。

8 参考资料

《高操控性中文网站工程建设手册》

(豆瓣8.7分, https://book.douban.com/subject/3132277/)

《高操控性中文网站工程建设高阶手册》

(豆瓣8.9分,https://book.douban.com/subject/4719162/ )

《高操控性JavaScript》

(豆瓣8.9分,https://book.douban.com/subject/5362856/ )

Google Developers

(https://developers.google.com/web/)

Efficient JavaScript

(https://dev.opera.com/articles/efficient-javascript/?page=3#reflow)

Best Practices for Speeding Up Your Web Site

(https://developer.yahoo.com/performance/rules.html )

热烈欢迎留言交流或投稿,和我们一起撷取知识。

Qunar技术沙龙

 前端性能优化

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务