性能优化
一、RAIL性能模型
1. 什么是RAIL
-
R - Response响应: 并非网络请求的响应,而是Web应用给用户的响应体验;
-
A - Animation动画:动画是为了提高用户体验,但是添加的动画是否流畅;
-
I - Idle空闲:让浏览器(主线程)有足够的空闲时间处理用户的交互,而不是一直在繁忙状态;
浏览器控制台有一个performance选项卡,列出了所有和性能相关的指标。
-
L - Load加载:网络加载
2. RAIL的目标
Rail的目标就是提高用户体验
3. RAIL评估标准
Google有庞大的用户群体,经常进行用户的问卷调查,得出了一些结论;
-
响应:处理时间应在50ms内完成。从用户输入到获得响应应在100ms内完成,但是浏览器处理用户输入是需要50ms左右的,所以保守估计程序处理的时间应在50ms内最好;
-
动画:每10ms产生一帧。为了保证浏览器的60帧的帧率,则每一帧的平均时间为16.67ms,但是浏览器每一帧的绘制会消耗6ms左右的时间,因此程序产生一帧画面的时间保守应在10ms内最好;
-
空闲:尽可能增加空闲时间。大量的运算和一些长时间占用计算资源的事情交给后端去做,前端需要留出足够的时间来处理用户的输入;
-
加载:在5s内完成内容的加载并且可以交互。这个要求很高,因为用户可能使用pc、移动端等设备,可能处在网络环境较差的情况,这是无法避免的。
这四个标准都是终极目标,同时满足这些的web应用堪称完美。
二、性能优化工具
1. Chrome Devtools
2. WebPageTest
- 在线网站测试:webpagetest.org
- 本地服务器,需要配置安装
3. Lighthouse
它是Google完成的一个开源项目。它默认是在移动端平台测试网页性能。
-
使用npm安装,然后使用lighthouse
npm i -g lighthouse lighthouse https://www.bilibili.com
-
查看本地报告
在运行完命令之后会将测试报告存在本地:
直接打开这个网页就能看到测试结果。而且还能看到他给的优化提示,会提示你做什么事情能优化大概多长时间。
- Chrome调试工具中集成了一个lighthouse。
三、常用的性能测量API
- 关键时间节点(Navigation Timing, Resource Timing)
- 网络状态(Network APIs)
- 客户端服务端协商(HTTP Client Hints)& 网页显示状态(UI APIs)
四、渲染优化
1. 现代浏览器网页渲染原理
关键渲染路径(critical rendering path):从触发到渲染的步骤。
- 拿到DOM后,可能会有JavaScript来实现页面上的视觉变化,比如增加删除DOM元、用JS来实现动画。除了JavaScript这一步还可能有CSS的动画、Web Application API 等可以触发视图变动;
- Style计算,第一部完成后,重新计算DOM元素的样式;
- 布局,这是一个几何问题,计算各个元素在视图中的什么位置,以及绘制的大小如何;
- 真正绘制元素,将文字、图片、颜色、阴影等绘制到视图上;
- 合成,浏览器为了提高效率,将不同的元素绘制到不同的图层上,最终展示的是各个图层进行合成之后的结果。
浏览器还有一些优化,那就是有的元素可以绕过一些关键渲染路径,比如有的元素不显示,或者不会影响布局,则在计算style包括布局绘制等可以绕过。
2. 重绘和回流
(1) 重绘
布局layout关心的是元素的几何大小,即元素的位置和大小,那么更改他的颜色、阴影大小等不会影响布局,那么更改这些CSS的时候,浏览器会绕过layout阶段,直接到paint阶段,就是重绘。
(2) 回流
更改一些CSS会影响到元素的布局,那么就需要进行回流,即渲染路径中必须要经过layout阶段。再次布局即为回流。
(3) 造成回流的操作
- 添加、删除元素;
- display : none, 这个和删除元素差不多;
- 移动元素的位置:位置变了是要重新计算布局的;
- 操作style可能造成回流;
- 修改offsetLeft、scrollTop、clientWidth;
- 修改浏览器的大小、字体的大小。
(4) 避免layout thrashing
-
避免回流:尽量不要做哪些引起回流的操作;
-
减少回流:像现在的很多前端框架,使用了虚拟DOM,将多次DOM样式或者DOM操作整理起来批量进行更新和删除,这样能有效地减少回流次数;
-
读写分离:将读取元素属性和修改元素属性的操作分开,批量读取,再批量写入。可以使用FastDom库进行读写分离操作。
(5) 减少重绘的方案
-
尽量使用transform而不直接去修改DOM的属性,可以不触发重绘和回流;
-
创建新的图层。
3. 复合线程与图层
复合图层(compositor thread) 图层(layers)
复合:把页面拆解成不同的图层,有时当某一图层变化的时候,不影响其他图层的渲染,这样绘制的过程会更高效。
-
复合线程在做什么:将页面拆分为图层进行绘制再进行复合。
- 默认情况下,浏览器根据自己的规则进行拆分,主要的分析是元素和元素之间是否会相互影响,如果某一个元素会对其他元素造成过多的影响,则最好将其提取;
- 也可以自己指定图层,z-index;
-
利用DevTools了解网页的图层拆分情况;
-
哪些样式仅影响复合,不触发重绘和回流:
尽量使用transform,可以不触发重绘和回流。
利用GPU加速:
GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率。
GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video)。
比如将2d transform转化为3d可以强制开启GPU加速,提高动画性能。
五、代码优化
1. JavaScript代码优化
-
JS的开销在哪里:加载、解析和编译、执行;
-
引用JavaScript库的优化:
- Code Splitting 代码拆分,按需加载;
- Tree Shaking 代码减重
-
减少主线程的工作量:
- 避免长任务;
- 避免超过1kb的行间脚本,因为行间脚本浏览器引擎是无法优化的;
- 使用rAF和rIC进行时间调度——requestAnimationFrame、requestIdleCallback
2. v8引擎编译原理
- 源码 => AST => 字节码Bytecode => 机器码
- 编译过程中会进行优化
- 运行时可能会进行反优化
3. v8的代表性的优化机制
-
脚本流:脚本在超过30kb的时候,认为脚本足够大,则新开一个线程对这个脚本进行解析,最后是多个解析结果的合并;
-
字节码缓存:源码在翻译成字节码之后,如果有些字节码重复使用,则将其缓存;
-
懒解析:针对函数而言,函数声明的时候不一定要立刻使用它,因此延迟对函数的解析直到使用之前。
对于函数的懒解析lazy parsing,有时候我们的代码定义了函数想要立刻使用,即我们想要eager parsing饥饿解析,这样反而降低了效率,我们就需要手动对定义的函数最外层加一对小括号(),告诉浏览器这个函数要使用eager parsing。
但是,项目往往会对JS代码进行压缩,在使用工具压缩的时候,往往会将这对()去掉,我们要使用一些工具将这对括号找回来,比如Optimize.js。
4. 对象优化
-
对象属性初始化顺序尽量保持一致:JS是弱类型的语言,引擎在解析的时候,会根据自己的推断进行解析,JS的隐藏类型有21种,声明对象、初始化值的时候会创建隐藏类型,隐藏类型会根据顺序进行缓存,如果下次赋值有新的属性或者属性的赋值顺序发生变化,则缓存失效;
-
对象实例化后尽量避免对其添加新的属性:实例化时已经存在的属性被称为In-Object属性,后添加的属性成为Normal/Fast属性,存储在property store里,需要通过描述数组间接查找,这样效率较低;
-
尽量使用Array来代替Array-like对象:类数组对象虽然有索引,有length属性,但是它不是数组,没有forEach等方法。JS引擎对数组有性能优化而类数组对象没有。所以大量操作的时候可以将类数组对象转化为Array对象再操作。
-
避免越界访问数组:越界的那个位置的值是undefined,数组也是对象,如果在当前位置找不到,则会沿着原型链想上找,这会造成额外的开销(差距为6倍)。此外还可能造成业务上的错误。
-
避免元素类型转换:当数组中元素的类型相同的时候,JS引擎会给这个数组标记一个元素类型,并且有相应的优化,如果加入一个不同类型的元素,那么值钱的优化都作废,并且数组的类型会降级。数组越通用,优化越少。
5. HTML优化
-
减少iframe的使用:额外添加的页面加载会阻塞原文档的加载,也就是说onload必然发生在iframe加载完成之后。而且在一个页面中使用iframe加载另一个页面比直接渲染另一个页面的开销要大。如果一定要使用iframe则考虑在onload事件之后为其添加src属性,再加载iframe;
-
压缩空白符:空白符虽然方便开发人员阅读,但是占用文档空间,所以在打包的时候最好去掉这些多余的空白符;
-
避免节点的深层级嵌套:节点层级越深,生成的DOM树就越深,遍历起来就越复杂。
-
避免使用table布局:使用起来也不灵活,开销也大;
-
删除注释;
-
CSS & JavaScript尽量外链:写在行间的话会造成HTML文档过大,而且引擎不好做优化;
-
删除元素的默认属性:也是一样,占多余空间;
-
借助工具:html-minifier
-
…
6. CSS优化
- 降低CSS对渲染的阻塞:尽量早的下载和解析CSS;
- 减小的CSS文件的大小:按需加载CSS,一开始只加载用到的CSS文件,而没有用到的文件则延迟下载解析;
- 利用GPU进行完成动画:利用关键渲染路径中的复合,不进行布局和重绘。单独将动画元素分成一个图层,GPU直接干预。
- 使用CSS的contain属性:contain 属性的主要目的是隔离指定内容的样式、布局和渲染。开发人员可以使用这个 contain 属性来限制指定的DOM元素和它的子元素同页面上其它内容的联系;
- 使用font-display属性:让文字尽早展示,且减轻文字闪动的问题。
六、资源的压缩合并
1. HTML资源压缩
- 使用在线工具进行压缩:http://kangax.github.io/html-minifier/
- 使用html-minifier等npm工具
2. CSS压缩
- 使用在线工具
- 使用clean-css等npm工具
3. JavaScript压缩与混淆
- 在线工具:现在js比较复杂,不太现实;
- Webpack对JS在构建时进行压缩。
4. CSS JS文件合并
- 若干小文件:考虑将他们合并;
- 文件和文件之间无冲突,服务相同的模块,可以合并;
- 如果仅仅是为了优化加载速度,则最好不要合并:现在的应用要让用户尽快的看到页面的渲染,分开加载可以做到,如果是较大的文件,可能出现等待。另外,还要考虑缓存的问题,小文件粒度小,利于缓存。
七、图片优化
1. 图片格式
-
jpeg | jpg
-
优点:很高的压缩比,而且画质和色彩还能较好的保存;
-
使用场景:需要展示比较大的图片,还想保存画质效果的时候,比如轮播图;
-
缺点:压缩比较高,如果想要展示清晰纹理和边缘的图片,会有锯齿感,比如logo
-
-
png
- 优点:可以做透明背景的图片,还可以强调纹理和边缘,弥补了jpg的缺点;
- 使用场景:贴图、小图片等;
- 缺点:细节保存好所以体积较大,色彩上和jpg不相上下。
-
WebP:
Google提出的一种新的图片格式。
-
优点:jpg一样的大小,png一样的质量。
-
兼容性:可能有10%的浏览器不兼容,慎用。
-
2. 图片加载优化
-
懒加载
-
渐进式图片
-
响应式图片:根据不同的设备加载不同质量的图片,结合设备dpi和窗口实际大小。
八、字体优化
1. FOIT和FOUT
- 字体为下载完成的时候,浏览器会对文字进行隐藏或对字体自动降级,导致字体闪烁;
- Flash Of Invisible Text:字体加载完成之前先隐藏文字;
- Flash Of Unstyled Text:字体加载完成之前先使用默认的已有的字体,加载完成后重新渲染文字。
2. font-display属性
@font-face中的属性
一共有五个值:auto、block、swap、fallback、optional,auto是让浏览器自动选择,没有意义。
3. 使用AJAX+Base64
- 优点:解决了兼容性的问题,把字体用Base64在后台转码,然后前端通过异步请求的方式获得字体,这样字体的格式没有兼容性的问题。另外,使用异步加载,可以推迟字体的加载时间;
- 缺点:由于字体通过Base64嵌入到资源文件(CSS文件中),那么就无法对字体进行缓存,字体的缓存就依赖于对CSS文件的缓存,本身不可控。