-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
前端性能优化相关 #34
Comments
前端性能优化最近工作中一个项目在运行时有一些性能问题,为此我看了很多与性能优化相关的内容,下面做个简单的分享。 前端性能优化,这包括 CSS/JS 性能优化、网络性能优化等等内容,这方面的内容 《高性能网站建设指南》、《高性能网站建设进阶指南》、《高性能JavaScript》 等等书都做了很多讲解,强烈推荐阅读。 下面的内容,上面提到的书中大都包含了,因此可以考虑转而去读这些书,做一个完完全全的了解,对于本文,也就不要再读下去了。 如果你坚持看到了这里,那就来谈谈我遇到的一些前端性能问题,并聊聊解决方案。 优先优化对性能影响大的部分当应用有了性能问题后,不要一股脑扎到代码中去,首先要想想那部分对性能影响最大。优先优化那些对性能影响大的部分,可以起到立杆见影的效果。 另外在对代码进行优化的时候,也首先要关注那些存在循环或者高频调用的地方。有的时候我们可能不知道某个地方是否会高频执行,比如某些事件的回调。这个时候可以使用 对高频触发的事件进行节流或消抖对于 Scroll 和 Touchmove 这类事件,永远不要低估了它们的执行频率,处理这类事件的时候可以考虑是否要给它们添加一个节流或者消抖过的回调。节流和消抖,可能其他人不这么翻译,其实也就是
使用
|
读书笔记 --《高性能 JavaScript》加载和执行javascript 是单线程的。且多数浏览器是使用单一进程来处理用户界面的刷新和javascript代码的执行的,所以 javascript 执行时间越久,页面等待时间就会越久。 在页面中每次出现 script 标签的时候,就意味着浏览器需要去执行这些代码,页面也就会处于暂停状况。为什么 javascript 的执行不能和页面的渲染并行呢,因为 javascript 的执行有时候决定了页面如何进行渲染。因为脚本可能在执行的过程中向页面中添加内容。 比如使用 使用 src 属性加载外部 javascript 文件的时候也是如此,浏览器必须等待文件加载完毕,并执行该文件,执行完毕后再继续解析 HTML 代码。在这个阶段用户的交互和页面渲染是完全被阻塞的。 脚本位置:浏览器在解析到 body 之前是不会开始渲染页面的,所以将大量的脚本或者样式表放在 head 部分会导致页面长时间处于空白。 file1.js —> load —> exec —> file2.js —> load —> exec —> file3.js —> load —> exec ok,虽然浏览器支持并行下载,但是执行上一定是串行的,所以尽可能将脚本放在最接近 body 底部的地方。 将内嵌脚本放在 head 中的情况为了确保脚本执行时候得到的是正确的样式,所以这会阻塞页面等到样式表的加载完毕。 脚本数量考虑到 http 连接的开销,可以将小的脚本文件进行压缩。 无阻塞的脚本javascript 倾向于阻塞用户界面和 HTTP 请求。减小 javascript 文件的大小,并减少http请求数量只是第一步。 延迟脚本defer ,async 属性 作用:异步加载脚本,不阻塞页面的渲染。保证脚本不会修改 DOM 目前所有主流浏览器都实现了以上属性的支持,有一点需要注意的是 defer 只作用于使用 src 加载的脚本 动态脚本
解决方法
DOM 编程使用脚本对 DOM 进行操作的代价很昂贵。 天生就慢因为 DOM 是独立于语言的,它提供了操作文档的 API,它和 ECMAScript 是分开的,它们两者就像是使用一座桥梁链接起来的一样,每当需要跨越这座桥梁的时候就要收取一些费用,所以要尽量减少过桥的次数。
元素集合使用 firstElementChild 等只获得元素节点的方法,而不是使用 firstChild 并判断其 nodeType 是否为 3 选择器APIquerySelectorAll 返回一个 NodeList 其中保存了所有符合选择器的元素的引用,这个集合不是动态的,不对随着文档的改变而改变。 重绘与重排浏览器在下载完成所有的资源之后,会建立两个内部的数据结构:
每当DOM元素的宽高,颜色等改变,都会触发渲染树的更新,对于尺寸变化会进行一次重新排版,然后在重新绘制。并不是任何操作都会触发重排,比如背景色的变化就只会触发一次重绘,因为布局并没有改变。 重排何时发生
渲染树变化的排队和刷新由于每次重排都会产生计算消耗,所以浏览器会通过队列来批量执行重排过程。然而在读取元素的 offset_。scroll_,client* 等属性的时候,由于要获取到当前准确的信息,这个时候会强制进行重排以返回正确值。 最小化重绘与重排
使用 el.style.cssText 来批量添加,或者在后面追加。
当需要对 DOM 进行一系列操作的时候,可以采用下面的方式:
其他一些方案:使用文档片段
因为在每次获取布局信息的时候都要进行渲染树的刷新,所以可以缓存下布局信息,避免频繁获取这些信息。
有的时候页面的顶部有一个动画,这个元素的高度不断变大,然后将下面的内容撑到下面,这个时候会导致整个页面的重绘,使得整个页面看起来一顿一顿的。解决方法是将这个元素在动画开始前设置为 绝对定位,让其脱离文档流,然后在应用动画,这个时候就只会对这个元素以及这个元素遮挡的元素进行重绘,在动画结束后再恢复定位让他回到标准流中。
对于元素很多的时候,不要对大量元素使用 hover 选择器。在 IE8 中性能很差。
利用事件冒泡的机制来处理大量的事件的绑定。 |
GPU 是如何加速网页渲染的前端工程师应该都听说过硬件加速,通常它是指利用 GPU 来加速页面的渲染。那么 GPU 目前在web页面的渲染过程中起到什么作用呢? GPU 的作用早期浏览器完全依赖 CPU 来进行页面渲染。现在随着 GPU 的能力增强和普及,且目前绝大多数运行浏览器的设备上都集成了 GPU。浏览器可以利用 GPU 来加速网页渲染。 GPU 包含几百上千个核心,但每个核心的结构都相对简单, GPU 的结构也决定了它适合用来进行大规模并行计算。进行图层合并需要操作大量的像素,这方面 GPU 能比 CPU 更高效的完成。这里有个视频,很清楚地说明 CPU 与 GPU 的差别。 页面渲染过程浏览器利用 HTML 构建出 DOM 树,利用 CSS 构建 CSSOM 树,最终得到 Render 树。 然而这只是很宏观的描述,浏览器为了将 DOM 元素高效地绘制且正确地出来,将多个元素安排在一个图层中,使用 PaintLayer 来描述,在每个 PaintLayer 中又存在 GraphicsLayers。当某个元素的样式改变后,不需要去重绘某个图层就好了。 浏览器的每一帧都可能会经过以下几个步骤: JavaScript 的执行可能修改 DOM 树和 CSSOM 树,随后浏览器需要重新计算样式,并根据新的样式计算出元素的实际属性(比如 CSS 中 width 是 50%,这里就要利用父元素的宽度得出自己真实的 width 值),重绘有变动的图层,随后将各图层传递给 GPU ,由 GPU 来进行图层的合并。 上面 5 个步骤中,Layout 和 Paint 是可以省略的,当修改后的样式不会改变元素的尺寸、位置等涉及布局的属性时候,就没有必要进行 Layout(计算布局),比如修改了 color 属性,这个时候就只需要进行重绘(Paint)步骤。同样的道理,修改某些属性也不需要进行 Paint 步骤,只需要 Composite 就可以。 因此,我们希望所做的操作能尽可能地避免 Layout 和 Paint 这两个步骤,这样一帧所需的时间也就会大大缩短,可以明显避免卡顿。 目前有三个属性的改变只需要进行 Composite 过程,分别是:
这几个属性的改变,GPU 只需要在合并图层之前对图层进行一些变换,比如 正确地利用 GPU使用 transform, filter 和 opacity 来完成动画使用以上 3 个属性来完成动画,可以避免在动画的每一帧进行重绘。如果在动画中改变了其他属性,那也不能避免重新绘制。 避免不合理地强制开启硬件加速常常看到有文章指出使用 使用 而如果整个图层的都要被重绘,那么再将其中的部分元素提升至单独的图层,会导致重绘的时候会分多个图层来进行绘制,然后在进行多个图层的合并,这个时候不如将所有元素放置在单个图层中,重绘整个大的图层。 总结所谓硬件加速,早起浏览器是使用纯软件来渲染页面的,如今现代浏览器利用了 GPU 来进行页面的渲染,在合适的时候浏览器就会自动去使用 GPU 而不是开发者自己去指定。GPU 的功能是在合并图层阶段,它可以在进行图层合并之前来对原图层进行一些变换,合理地使用这个变换可以避免页面重绘,使得每一帧消耗的时间最少,避免卡顿。 |
避免强制性同步布局强制性同步布局,发生在使用 JavaScript 改变了 DOM 元素的属性,而后又读取 DOM 元素的属性。比如改变了 DOM 元素的宽度,而后又使用 案例想象一下,如果有一组 DOM 元素,我们需要读取它们的宽度,并设置其高度与宽度一致。 解决方案1. 新手解决方法for(var i = 0,len = divs.length; i<len; i++){
var width = divs[i].clientWidth;
divs[i].style.height = width + 'px';
} 执行这段代码就引起了强制性同步布局(forced synchonous layout),在每次迭代开始的时候都会进行重新计算布局,这是很昂贵的操作,千万要避免。 2. 分离读和写以上场景下,我们可以使用两次循环,在第一次循环中只进行读取 DOM 元素宽度的操作,并将结果保存起来,在第二个循环中修改 DOM 元素的高度。 var widthArray = [];
for(var i = 0,len = divs.length; i<len; i++){
var width = divs[i].clientWidth;
widthArray.push(width);
}
for(var i = 0,len = divs.length; i<len; i++){
divs[i].style.height = widthArray[i] + 'px';
} 3. 使用
|
这里实在抱歉,为了保证连贯性,我删除了两个点赞的评论 How To Reach 60FPS这篇文章是我为了完成一次关于前端性能的分享而写下的,你可以在这里看到我分享时的 PPT。 什么是 fps,60fps 意味着什么?fps(frames per second),指一秒内屏幕刷新的次数或者动画在一秒内更新的帧数。现代浏览器大多每秒刷新 60 次,为了和设备的刷新频率保持一致,动画也要保证每秒 60 更新帧。如果低于 60 fps,称动画发生了掉帧,如果掉帧严重,用户则能够明显地感觉到卡顿。高的帧率,意味着更连贯的动画,更流畅的滚动,这些总是能带来极好的用户体验。 构建 DOM 树、CSSOM 树、渲染树要想高效地操作 DOM, 完成流畅的动画,需要了解浏览器是如何将 HTML/CSS/JavaScript 等资源渲染为 Web 页面的。下面就此过程进行描述: 浏览器接收到 HTML 文档后就会开始解析文档,并建立 DOM 树 (Document Object Model Tree),DOM 树中记录了当前文档的所有节点。同时浏览器使用内联的 style 标签或者外部加载的 CSS 文档来构建 CSSOM 树(CSS Object Model Tree),CSSOM 树中记录了各个节点的样式规则。随后联合 DOM 树和 CSSOM 树构建出渲染树(Render Tree),渲染树中记录了当前页面中所有可见节点的实际样式。之所以说实际样式,是因为 CSS 中可能出现 整个步骤,如下图所示:
得到了渲染树,浏览器还不能开始进行绘制,因为页面上存在太多元素,如果页面中有一个元素被改变,这个时候如果重绘整个页面就显得很浪费,毕竟很多时候只是很小的一部分被改变了。浏览器为了高效地绘制,提出了图层(layer)的概念,按照某些规则将 DOM 节点划分在不同的图层中,这样一个节点的改变,浏览器会智能地去重绘那些受到影响的图层,而非所有图层,浏览器绘制的时候是以图层为单位的。 细分后的过程,大致是这样: 绘制过程就是浏览器调用绘图 API 来完成图层的绘制,绘制过程就是填充像素的过程,浏览器会调用一些类似于 综上,浏览器渲染出 Web 页面的过程,大体可分为以下几个步骤:
可以想象浏览器内部实现原本以上论述复杂千万倍,以上也只是从非常宏观的角度去描述了浏览器渲染页面的过程。其中还没牵扯到 JavaScript,不过知道以上这些内容,起码对浏览器的渲染流程有了一个大体的认识。 浏览器在每一帧中要做的工作JavaScript 通过 API 来修改 DOM 树和 CSSOM 树,CSS 中的 animation 或 transition 都会改变渲染树,每当渲染树被改变后,浏览器都需要重新计算样式,样式计算会涉及多个 DOM 节点,因为有些样式存在继承关系,还有则是相对父节点的。 每一帧中浏览器都 可能 要进行下列部分或全部步骤: 对上图中的各个步骤进行一个简要的解释说明:
在 Chrome DevTools 可以清楚地看到这几个步骤: 部分步骤可以被跳过如果修改了一个会影响元素的尺寸或位置的属性,比如 width 和 height 或者 top 等,需要重新进行 Layout 操作,随后会进行重绘,随后将图层合并得到新一帧。这就会执行以上的所有步骤。 但如果只是修改了 color 这样的不涉及节点尺寸或定位的属性,则不需要执行 Layout 这一步骤。因为 color 的修改,并不会影响元素的尺寸和位置,只需要进行一次重绘就好了,此时以上步骤中的 Layout 就被跳过了。 同样的,如果修改了一个都不需要进行重绘的属性,那么可以跳过 Layout 和 Paint 这两个步骤,此时只需要要进行图层的合并操作就能得到新一帧的图片。 不需要进行重排(Layout)和重绘(Paint)操作,自然会耗时更短,每一帧中浏览器需要进行的工作也就越少,一定程度上也就能够提升性能。由此看来对 DOM 树的修改、对 DOM 节点属性或样式的修改,需要付出的代价是不同的,某些操作可能会触发重排和重绘操作,而有些操作则可以完全跳过以上步骤。 规律不过也可以得出如下的一个规律:
参考资料paul irish 罗列了那些操作会触发重排,你可以在这里看到: What forces layout / reflow 另外在 https://csstriggers.com/ 这个网站上,Chrome 团队的一伙人列出了对 CSS 各属性的修改会引发以上那些操作。 在实践中可以时刻参考这两个列表,并结合调试工具,来避免没有不要的重排和重绘。 Reach 60fps前面介绍了不少关于浏览器渲染过程的基础知识,旨在帮助对此不清楚的朋友从宏观上理清楚 Web 页面的渲染过程。 实现连贯的动画,流畅的滚动,了解以上基础知识对后续编码、优化有着巨大的好处。下面根据浏览器渲染原理,结合每一帧的浏览器需要做的各个步骤,给出了一些切实可行的优化方案,并提出一些注意事项。 后面的内容我想分 5 个点来介绍,分别是:
1. 避免没有必要的重排每个前端工程师在入门的时候,都被告知 DOM 很慢,使用脚本对 DOM 进行操作的代价很昂贵,要批量修改 DOM 等等,关于 DOM 操作的话题已经有不少著作进行过论述了。强烈推荐 《高性能 JavaScript》 这本书,我觉得这本书应该是前端工程师必读。 虽说已经有很多关于 DOM 操作的内容了,这里我还是想提一个注意事项:避免强制性同步布局,因为我经常看到这个字眼,不妨提出来谈谈。 避免强制性同步布局强制性同步布局(forced synchonous layout),发生在使用 JavaScript 改变了 DOM 元素的属性,而后又读取 DOM 元素的属性的时候,通常也说读取了脏 DOM 的时候。比如改变了 DOM 元素的宽度,而后又使用 设想以下案例,有一组 DOM 元素,需要将其其高度设为与宽度一致,新手很快就能写出以下代码: 解决方案 1 - 简单粗暴: for(var i = 0,len = divs.length; i<len; i++){
var width = divs[i].clientWidth;
divs[i].style.height = width + 'px';
} 执行这段代码的时候,每次迭代开始的时候,DOM 都是脏的(被改动过),为了获得真实的 DOM 尺寸,都会重新计算布局。该循环就会引发多次强制性同步布局,这是很低效的做法,千万要避免。 从 Chrome DevTools 中很容易地发现该低效操作,可以看到浏览器进行了很多次的重新计算样式(Recalculate Style)和布局(Layout),也叫做 reflow(重排)的操作,且这一帧用时很长。 解决方案 2 - 分离读和写: 可以很轻松地解决这个问题,使用两次循环,在第一次循环中读取 DOM 元素宽度并将结果保存起来,在第二个循环中修改 DOM 元素的高度。 var widthArray = [];
for(var i = 0, len = divs.length; i<len; i++){
var width = divs[i].clientWidth;
widthArray.push(width);
}
for(var i = 0, len = divs.length; i<len; i++){
divs[i].style.height = widthArray[i] + 'px';
} 分离读写,一个时刻只读取,另一个时刻只改写,这样就能很有效地避免强制性同步布局。 在实际项目中往往没有上面提到的那样简单,有时尽管已经分离了读和写,但在写操作后面还是不可避免地存在读取操作,这个时候不妨将写操作放在 补充资料
2. 避免没有必要的重绘在开始之前需要回顾一下什么时候需要重绘:
在 Chrome DevTools 的 Rendering 选择卡中勾选 Painting Flashing 选项后,可以观察到页面上正在进行重绘的区域。 避免 fixed 定位元素在滚动时重绘一个常见的场景是,网页有一个 fixed 定位的头部导航栏或者侧边栏。问题存在于每次滚动后,这些 fixed 定位的元素相对于整个内容区域的位置改变了。这就相当于一个图层中的某个元素的位置改变了,为了获得滚动后的图层,需要进行重绘,因此每次滚动都会进行重绘操作。 举个例子,在腾讯网首页上有如下 fixed 定位的元素: 不幸的是这几个 fixed 定位的元素和整个网页位于同一个图层: 滚动后,因为定位元素相对于整个文档的位置发生了改变,因此整个文档都需要被重绘。解决此类问题的方法就是将 fixed 定位的元素提升至单独的图层。使用 注:Chrome 在高 dpi 的屏幕上会自动将 fixed 定位的元素提升至单独的图层,在低 dpi 的屏幕上不会提升,因此很多开发者在 MacBook Pro 上测试的时候,不会发现问题,但用户在低 dpi 的屏幕上访问的时候就出问题了。 将部分元素提升至单独图层,避免大面积重绘使用 这是一个面板,其中内容区域的文字会不断地闪烁(文本的颜色会改变),如果将该文本使用 正确地处理动图页面加载的时候为了更好的用户体验常常会使用一个 loading,但在页面加载完成后如何处理 loading 呢?一个错误的方法是将其 z-index 设置一个更小的值,将其隐藏起来,不幸的是就算 loading 不可见,浏览器依然会在每一帧对它进行重绘。因此对于像 loading 这样的动态图,在不需要显示的时候最好使用 3. 利用 GPU 加速网页渲染前端工程师应该都听说过硬件加速,通常是指利用 GPU 来加速页面的渲染。早期浏览器完全依赖 CPU 来进行页面渲染。现在随着 GPU 的能力增强和普及,且目前绝大多数运行浏览器的设备上都集成了 GPU。浏览器可以利用 GPU 来加速网页渲染。 GPU 包含几百上千个核心,但每个核心的结构都相对简单, GPU 的结构也决定了它适合用来进行大规模并行计算。进行图层合并需要操作大量的像素,这方面 GPU 能比 CPU 更高效的完成。这里有个视频,很清楚地说明 CPU 与 GPU 的差别。 常常看到有文章指出使用 何为硬件加速GPU 能够存储一定数量的纹理(texture),也就是一个矩形的像素点集合。通常这个集合会对应到 Web 页面上的某个图层,GPU 能够高效地对这些像素点进行多种变换(位移、旋转、拉伸)操作。在实现动画的时候,利用 GPU 的这一特性,如果只需要对原像素集合在 GPU 内进行一次变换,就能得到新一帧的图层,那么动画的所有操作都在 GPU 内高效地完成了,没有重绘操作。 得到了变换后的图层,只需要再进行一次图层的合并,将该变换后的图层和其他图层合并起来,最终得到在屏幕上显示的整幅图片。GPU 的这一特性就常常被称为硬件加速。 要利用硬件加速也是有条件的,盲目地使用 使用 目前常见的 CSS 属性中只有 filter, transform, opacity 这几个属性的改变可以在 GPU 端进行处理,这在前面已经提到过了,因此应该尽可能使用这些属性来完成动画。 后面会有更多关于利用 GPU 的这一特性的例子,下面先看一个需要注意的点: 避免无谓地新建图层一个真实案例: 这是一个城市选择页,这个页面中的每一项都使用了 从上图中可以看到,性能是相当糟糕的,大量时间都花费在了图层的合并上,每一帧都需要合并上千个列表子项,这不是一件很轻松的事情。 为了体现,错误使用 因此在谈起硬件加速的时候,一定知道,什么是硬件加速,硬件加速是如何工作的,它能做什么,不能做什么。合理的利用 GPU 才能利用它帮我们构建出 60fps 的体验。 4. 构建更加流畅的动画上面讲了,使用 transform 和 opacity 来创建动画(filter 的支持度还不够好)最为高效。因此每当需要用到动画的时候,首先要考虑使用这两个属性来完成, 避免使用会触发 Layout 的属性来进行动画有时候看起来不太可能使用这两个属性来完成,不过仔细想想往往能够想到解决方案。考虑下面动画: demo 地址:expand cord 一般的想法可能是修改每个卡片的 // 拿到初始尺寸
let first = card.getBoundingClientRect();
// 加上最终状态的类名
card.classList.add('card--expand');
// 拿到最终尺寸
let last = card.getBoundingClientRect();
// 设置形变参考点
card.style.transformOrigin = '0 0';
// 计算需要位移和伸缩的量
let transform = {
x: first.left- last.left,
y: first.top - last.top,
sx: first.width / last.width,
sy: first.height / last.height
};
// 加上 transform 将其从最终状态缩小到最初状态
card.style.transform = `translate(${transform.x}px, ${transform.y}px)
scale(${transform.sx},${transform.sy})`;
// 在下一帧
requestAnimationFrame(function(){
// 加上过渡时间
card.style.transition = 'transform .4s cubic-bezier(0,0,0.3,1)';
// 将 transform 取消,这样就会慢慢过渡到最终状态
card.style.transform = 'none';
}); 以上思路是使用 经过这样的处理,原本需要使用 使用 transform, filter 和 opacity 来完成动画使用以上 3 个属性来完成动画,可以避免在动画的每一帧进行重绘。但如果在动画中改变了其他属性,那也不能避免重新绘制。要尽可能地利用这几个属性来完成动画。涉及位移的考虑使用 translate,涉及大小的考虑 scale,涉及颜色的考虑 opacity,为了实现流畅的动画要想尽一切办法。 这里给出一个案例,Instagram 的安卓 APP 在登录的时候,有一个颜色渐变的效果,这种效果常常见到。 通过地不断地改变背景颜色能很快地实现,测试后会发现在低端设备上会感到卡顿,CPU 使用率飙升,这是因为修改背景颜色会导致页面重绘。为了不重绘也能达到同样的效果,我们可以使用两个 div,给它们设置两个不同的背景色,在动画中改变两个 div 的透明度,这样两个不同透明度的 div 叠加在一起就能得到一个颜色演变的效果,而整个动画只使用了 opacity 来完成,完全避免了重绘操作。 关于示例,你可以在此处看到: 使用 background 完成渐变 vs 使用 opacity 完成渐变 不要混用 transform, filter, opacity 和其他可能触发重排或重绘的属性,虽然使用 transform, filter, opacity 来完成动画能够有很好的性能,但是如果在动画中混合使用了其他的会触发重排或重绘的属性,那么依然不能达到高性能。 使用
|
目录
更多内容:
The text was updated successfully, but these errors were encountered: