前端性能优化理论与实践
前端性能优化理论与实践
Web性能
Web性能是网站或应用程序的客户衡量度和用户的体验。
- 减少总体负载时间:使文件尽可能小,尽可能减少HTTP请求的次数,并采用巧妙的加载技术使文件更快可用
- 尽快使网站可用:以合理的顺序加载网络资源,用户进行执行主要任务的同时,任何其他资源都可以在后台继续加载。或等到实际需要时才加载资源——懒加载,网站从开始加载,到达到可用状态为止所需要的时间被称为交互等待时间 Time To Interactive
- 流畅性和交互性:尽量减少由于DOM变化而引起的重绘UI的次数
- 感知性能:用户感受到的性能与任何客户统计数据一样重要,不是度量标准。即使操作花费很长时间,合理的过渡或者提示技巧来保持用户在等待期间的参与度。
- 性能测量:Web性能包括测量应用程序的实际速度和感知速度,在可能的情况下进行优化,然后监视性能,以确保优化过的内容保持优化状态。
总而言之,许多特性都会影响性能。包括延迟、应用程序大小、DOM节点数量、资源请求数量、JavaScript性能、CPU负载等。重要的是尽量缩短加载和响应的时间,并通过增加额外的功能来隐藏延迟;先使网站变得可以交互,同时在后台加载用户体验中的次要部分
内容如何渲染
浏览器如何工作的细节:
- 浏览器的工作原理
- 源顺序:HTML索引文件的源顺序会显著影响性能。从索引文件链接到的附加资源的下载通常基于引用的顺序,这是可以调整的,需要进行优化。一些资源在被解析和执行完成之前,会阻塞其他的下载
- 关键路径:这是从服务器下载文件后,浏览器用来构建web文档的过程。浏览器按照明确定义的步骤进行操作,优化关键路径以优化显示与当前用户操作相关的内容,将显著改善内容呈现时间。
- 文档对象模型:文档对象模型DOM是一种树形结构,将HTML的内容和元素表示为节点树。这包括所有HTML属性和节点之间的关系。加载页面后的大量DOM操作可能会影响性能。因此有必要了解DOM的工作原理,减轻此类问题
- 延迟:延迟是网站信息从服务器传输到用户计算机所需要的时间,建立TCP和HTTP连接需要消耗系统资源,并且在网络上来回传输请求和响应字节时不可避免地存在延迟。有一些方法可以减少延迟,如通过下载更少的文件来减少HTTP请求的数量,使用CDN使站点在全球范围内具有更普遍的性能,并使用HTTP/2协议提高传输的效率
- CDN内容分发网络指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务可以根据哪些服务器与用户距离最近,来满足数据的请求。CDNs提供快速服务,较少受高流量影响。CDNs被广泛用于传输 stylesheets和JavaScript等静态资源,像Bootstrap/Jquery等,这些库文件使用CDN技术
- 通过CDN向用户分发传输相关的静态资源文件,可以降低我们自身服务器的请求压力
- 大多数CDN在全球都有服务器,所以CDNs上的服务器在地理位置上可能比自己的服务器更接近用户,地理距离会按比例影响延迟
- CDNs已经配置了恰当的缓存设置,使用CDN节省了在服务器中对静态资源文件的配置
- CDN内容分发网络指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务可以根据哪些服务器与用户距离最近,来满足数据的请求。CDNs提供快速服务,较少受高流量影响。CDNs被广泛用于传输 stylesheets和JavaScript等静态资源,像Bootstrap/Jquery等,这些库文件使用CDN技术
浏览器的工作原理
等待资源加载时间和大部分情况下的浏览器单线程执行是影响Web性能的两大主要原因
为了实现快速加载,尽可能快的发送请求的信息,网络等待时间是在链路上传送二进制到电脑端所消耗的链路传输时间。Web性能优化需要尽可能快的使页面加载起来。
大部分情况下,浏览器是单线程执行的。为了有流畅的交互,开发者的目标是确保网站从流畅的页面滚动到点击响应的交互性能。渲染时间是关键要素,确保主线程可以完成所有给它的任务并且仍然一直可以处理用户的交互。通过了解浏览器单线程的本质与最小化主线程的责任可以优化Web性能,来确保渲染的流畅和交互响应的及时。
导航
导航是加载web页面的第一步,Web性能优化的目标之一就是缩短导航完成所花费的时间,等待时间和带宽会导致它的延时
DNS查询
对于一个web页面来说导航的第一步是去寻找页面资源的位置,例如导航到 https://example.com
HTML页面被定为到IP定制为 93.184.216.34
的服务器。如果之前没有访问这个网络,就需要进行DNS查询。
浏览器向名称服务器发起DNS查询请求,最终得到一个IP地址。第一次请求之后,这个IP地址可能会被缓存一段时间,这样可以从缓存里面检索IP地址而不是再通过名称服务器进行查询来加速后续的请求。
通过主机名加载一个页面通常仅需要一次DNS查询,但是对于页面指向的不同的主机名,则需要多次DNS查询,如果字体、图像、脚本、广告和网络统计都有不同的主机名,则需要对每一个主机名进行DNS查询。
DNS查询可能存在性能问题,特别是移动网络,当一个用户使用了移动网络,每一个DNS查询从手机发送到基站,然后到达一个认证的DNS服务器。手机、信号塔、名称服务器之间的距离可能是一个大的时间等待。
TCP握手
一旦获取到服务器IP地址,浏览器就会通过TCP三次握手与服务器建立连接。这个机制是用来让两端尝试进行通信——在浏览器和服务器通过上层协议HTTPS发送数据之前,可以协商网络TCP套接字连接的一些参数。
TCP的三次握手技术经常被称为“SYN/SYN-ACK/ACK”,因为通过TCP首先发送了三个消息进行协商,然后在两台电脑之间开始一个TCP会话,意味着终端与每台服务器之间还要来回发送三条消息,而请求尚未发出。
TLS协商
对于通过HTTPS建立的安全连接,还需要另一次握手,叫做TLS协商,决定使用哪种密码对通信进行加密,验证服务器,并在开始实际数据传输前建立安全连接。这就需要在实际发送内容请求之前,再往返服务器五次
传输层安全协议 Transport Layer Security,缩写作TLS, 前身是安全套接层Secure Sockets Layer, 缩写为SSL, 是一个被应用程序用来在网络中安全通信的protocol通讯协议,防止电子邮件、网页、消息以及其他协议被篡改或是窃听。
所有现代浏览器都支持TLS协议,都要求服务器提供一个有效的digital certificate数字证书来确认身份以建立安全连接,如果客户端和服务器都能提供自己的数字证书,则它们可以互相认证
虽然建立安全连接对增加了加载页面的等待时间,对于建立一个安全的连接来说,以增加等待时间为代价是值得的,因为在浏览器和web服务器之间传输的数据不可以被第三方解密
经过8次往返,浏览器终于可以发出请求
响应
一旦我们建立了到web服务器的连接,浏览器就代码用户发送一个初始的HTTP GET请求,对于网络来说,这个请求通常是一个HTML文件,一旦服务器收到请求,它将使用相关的响应头和HTML的内容进行回复。
初始请求的响应包含所接收数据的第一个字节。首字节时间TTFB Time To First Byte是用户通过点击链接进行请求与收到第一个HTML数据包之间的时间,第一个内容分块通常是14KB的数据。
拥塞控制/TCP慢启动
TCP数据包在传输过程中被分成若干段,由于TCP保证数据包的顺序,因此服务器在发送一定数量的数据包后,必须以ACK数据包的形式收到客户端的确认。
如果服务器在每个网段后都等待ACK则会导致客户端频繁发出ACK, 即使在网路负荷较低的情况下也会增加传输时间。
另一方面,一次性发送过多网段可能会导致客户端无法接收到网段,只能长时间不停回应ACK, 服务器不得不不断重新发送网段。为了平衡传输段的数量,TCP慢启动算法用于逐渐增加传输数据量,直到确定最大网络带宽,并在网络负载较高时减少传输数据量。
传输段的数量由拥塞窗口CWND的值控制,该值可初始化为1、2、4或10MSS以太网协议中为1500字节。该值是发送的字节数,客户端收到后必须发送ACK。
如果收到ACK, 那么CWND值将加倍,这样服务器下次就能发送更多的数据段,如果没有收到ACK就减半,因此,这种机制在发送过多网段和过少网段之间取得平衡。
解析
一旦浏览器收到数据的第一块,就可以开始解析收到的信息。解析是浏览器将通过网络接收的数据转换为DOM和CSSOM的步骤,通过渲染器把DOM和CSSOM在屏幕上绘制成页面。
DOM是浏览器标记的内部表现。DOM也是被暴露的,可以通过JavaScript 中的各种API进行DOM操作。
即使请求页面的HTML大于初始的14K数据包,浏览器也将开始解析并尝试根据其拥有的数据进行渲染。这是为什么在前14KB中包含浏览器开始渲染页面所需的所有内容,或者至少包含页面模板第一次所需要CSS和HTML对于web性能优化来说是重要的。但是在渲染到屏幕上面之前,HTML/CSS/JS必须被解析完成。
构建DOM树
第一步是处理HTML标记并构造DOM树,HTML解析涉及到符号化和树的构造。HTML标记包括开始和结束标记,以及属性名和值,如果文档格式良好,则解析它会简单而快速。解析器将标记化的输入解析到文档中,构建文档树。
DOM树描述了文档的内容,html元素是第一个标签也是文档树的根节点,树反映了不同标记之间的关系和层次结构。嵌套在其他标记中的标记是子节点,DOM节点的数量越多,构建DOM树所需要的时间越长。
当解析器发现非阻塞资源,例如一张图片,浏览器会请求这些资源并且继续解析,当遇到一个CSS文件时,解析也可以继续进行,但是对于script标签,特别是没有async或者defer的属性会阻塞渲染并停止HTML解析。尽管浏览器的预加载扫描器加速了这个过程,但过多的脚本仍然是一个重要的瓶颈。
预加载扫描器
浏览器构建DOM树时,这个过程占用了主线程。当这种情况发生时,预加载扫描仪将解析可用的内容并请求高优先级资源,如CSS/JavaScript/web字体。我们不必等到解析器找到对外部资源的引用来请求它,预加载扫描器将在后台检索资源,以便在主HTML解析器到达请求的资源时,它们可能已经在运行,或者已经被下载。预加载扫描仪提供的优化减少了阻塞。
<link rel="stylesheet" href="styles.css" />
<script src="myscript.js" async></script>
<img src="myimage.jpg" alt="图像描述" />
<script src="anotherscript.js" async></script>
上面例子中,当主线程在解析HTML和CSS时,预加载扫描将找到脚本和图像,并开始下载它们。为了确保脚本不会阻塞进行,当JavaScript 解析和执行顺序不重要时,可以添加async和defer属性。
等待获取CSS不会阻塞HTML的解析或者下载,但是它确实会阻塞JavaScript,因为JavaScript经常用于查询元素的CSS属性。
构建CSSOM树
第二步是处理CSS并构建CSSOM树。CSS对象模型和DOM是相似的。DOM和CSSOM是两棵树。它们是独立的数据结构。浏览器将CSS规则转换为可以理解和使用的样式映射。浏览器遍历CSS中的每个规则集,根据CSS选择器创建具体父子和兄弟关系的节点树。
与HTML一样,浏览器需要将接收到的CSS规则转换为可以使用的内容。因此它重复了HTML到对象的过程,但对于CSS, CSSOM树包括来自用户代理样式表的样式。浏览器从适用于节点的最通用规则开始,并通过应用更具体的规则递归地优化计算的样式。
构建CSSOM非常快,并且在当前开发工具中没有一独特的颜色显示,创建CSSOM的总时间通常小于一次DNS查询所需要的时间
其他过程
JavaScript编译
在解析CSS和创建CSSOM的同时,包括JavaScript文件在内的其他资源也在下载,归功于预加载扫描器。JavaScript会被解析、编译和解释。脚本被解析为抽象语法树,有些浏览器引擎也会将抽象语法树输入编译器,输出字节码。这就是所谓的JavaScript编译,大部分代码都是在主线程上解释的
构建无障碍树
浏览器还构建辅助设备用于分析和解释内容的无障碍树。无障碍对象模型AOM类型DOM的语义版本。当DOM更新时,浏览器会更新辅助功能树。辅助技术本身无法修改无障碍树。
在构建AOM之前,屏幕阅读器无法访问内容。
渲染
渲染步骤包括样式、布局、绘制,在某些情况下还包括合成。在解析步骤中创建的CSSOM树和DOM树组合成一个渲染树,然后用于计算每个可见元素的布局,然后将其绘制到屏幕上。在某些情况下,可以将内容提升到它们自己的层并进行合成,通过在GPU而不是CPU上绘制屏幕的一部分来提供性能,从而释放主线程。
样式
关键呈现路径的第三步是将DOM和CSSOM组合成渲染树。计算树或渲染树的构建从DOM树的根开始,遍历每个可见节点。
不会被显示的元素,如head元素及其子元素,以及任何带有 display:none
的节点,如用户代理样式表中 script { display: none; }
, 都不会包含在渲染树中,因为它们不会出现在渲染输出中。应用了 visibility: hidden
的节点会包含在渲染树中,因为它们会占用空间。由于我们没有给出任何指令来覆盖用户代理默认值,因此上述代码示例中的 script
不会包含在渲染树中。
每个可见节点都应用了CSSOM规则。渲染树包含所有可见节点的内容和计算样式,将所有相关样式与DOM树中的每个可见节点匹配起来,并根据CSS级联,确定每个节点的计算样式。
CSS级联(层叠)是一个基本特征,定义了如何合并来自多个源的属性值的算法,在CSS处于核心地位。
布局
第四步是在渲染树上运行布局以计算每个节点的几何体。布局是确定呈现树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。
构建渲染树后,开始布局。渲染树标志显示哪些节点(即使不可见)及其计算样式,但不标识每个节点的尺寸或位置。为了确定每个对象的确切大小和位置,浏览器从渲染树的根开始遍历它。
在网页上,大多数东西都是一个盒子,不同的设备和不同的桌面意味着无限数量的不同的视区大小。在此阶段,考虑到视口大小,浏览器将确定屏幕上所有不同框的尺寸。以视口的大小为基础,布局通常从body开始,用每个元素的框模型属性排列所有body的子孙元素的尺寸,为不知道其尺寸的替换元素提供占位符空间。
第一次确定节点的大小和位置成为布局。随后节点的大小和位置的重新计算称为回流。在示例中,假设初始布局开始发生在返回图像之前。由于没有声明图像的尺寸,因此一旦知道图像的尺寸,就会出现回流。
绘制
最后一步是将各个节点绘制到屏幕上,第一次出现的节点称为 first meaningful paint。在绘制或光栅化阶段,浏览器将在布局节点计算的每个框转换为屏幕上的实际像素。绘画包括将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)。浏览器需要非常快地完成这项工作。
为了确保平滑滚动和动画,占据主线程的所有内容,包括计算样式,以及回流和绘制,必须让浏览器在16.67毫秒内完成。在2048x1536分辨率的Ipad上,有超过314.5万像素将被绘制到屏幕上。那是很多像素需要快速绘制。为了确保重绘的速度比初始绘制的速度更快,屏幕上的绘图通常被分解成数层。如果发生这种情况,则需要进行合成。
绘制可以将布局树中的元素分解为多个层。将内容提升到GPU上的层而不是CPU上的主线程可以提高绘制和重新绘制性能。有一些特定的熟悉和元素可以实例化一个层,包括video/canvas,任何CSS属性为opacity, 3D transform/will-change的元素,还有一些其他元素。这些节点将于子节点一起绘制到它们自己的层上,除非子节点由于上述一个或者多个原因需要自己的层。
合成
当文档的各个部分以不同的层绘制,相互重叠时,必须进行合成,以确保它们以正确的顺序绘制到屏幕上,并正确显示内容。
当页面继续加载资源时,可能会发生回流,回流会触发重新绘制和重新组合,如果我们定义了图像的大小,就不需要重新绘制,只需要重新绘制需要重新绘制的层,并在必要时进行合成。但我们没有包括图像的大小。从服务器获取图像后,渲染过程将返回到布局步骤并从那里重新开始。
交互
一旦主线程绘制页面完成了,如果加载包含JavaScript并且延迟到onload事件触发后执行,则主线程可能很忙,无法用于滚动、触摸和其他交互。
可交互时间TTL Time To Interactive 是测量从第一个请求导致DNS查询和SSL连接到页面可交互时所用的时间——可交互时 First Contentful Paint 之后的时间点,页面在50ms内响应用户的交互。如果主线程正在解析、编译和执行JavaScript,则它不可用,因此无法及时小于50ms响应用户交互。
示例中,可能图像加载很快, anotherscript.js
可能是2MB, 而且用户的网络连接慢,在这种情况下,用户可以非常快地看到页面,但是在下载、解析和执行脚本之前,就无法滚动,这不是一个好的用户体验,避免占用主线程。
关键渲染路径Critical Render Path
关键渲染路径是将浏览器HTML, CSS和JavaScript转换为屏幕上的像素所经历的步骤序列。优化关键渲染路径可以提供渲染性能。关键渲染路径包含了文档对象模型DOM, CSS对象模型CSSOM, 渲染树和布局。
在解析HTML时会创建文档对象模型。HTML可以请求JavaScript,而JavaScript反过来,又可以更改DOM。HTML包含或请求样式,依次来构建CSS对象模型。浏览器引擎将两者结合起来创建渲染树。布局确定页面上所有内容的大小和位置,确定布局后,将像素绘制到屏幕上。
优化关键渲染路径可以缩短首次渲染的时间,了解和优化关键渲染路径对于确保重排和重绘可以每秒60帧的速度进行,以确保高效的用户交互并避免卡顿是很重要的。
理解CRP
Web性能包含了服务器请求和响应、加载、执行脚本、渲染、布局和绘制每个像素到屏幕上。
网页请求从HTML文件请求开始。服务器返回HTML——响应头和数据。然后浏览器开始解析HTML, 转换收到的数据为DOM树。浏览器每次发现外部资源就初始化请求,无论是样式、脚本或者嵌入的图片引用。有时请求会阻塞,这意味着解析剩下的HTML会被终止直到重要的资源被处理。浏览器接着解析HTML发请求和构造DOM直到文件结尾,这时开始构造CSS对象模型。等到DOM和CSSOM完成之后,浏览器构造渲染树,计算所有可见内容的样式。一旦渲染树完成布局开始,定义所有渲染树元素的位置和大小。完成之后,页面被渲染完成,或者说绘制到屏幕上。
文档对象模型
DOM构建是增量的。HTML响应变成令牌token,令牌变成节点,而节点又变成了DOM树。单个DOM节点以startTag令牌开始,以endTag令牌结束。节点包含有关HTML元素的所有相关信息。该信息是使用令牌描述的。节点根据令牌层次结构连接到DOM树中。如果另一组startTag个endTag令牌位于一组startTag和endTag之间,则在节点内有一个节点,这是定义DOM树层次结构的方式。
节点数量越多,关键渲染路径中的后续事件将花费的时间越长。
CSS对象模型
DOM包含页面所有的内容。CSSOM包含了页面所有的样式,也就是如何展示DOM的信息。CSSOM跟DOM很像,但是不同。DOM构造是增量的,CSSOM却不是。CSS是阻塞渲染的:浏览器会阻塞页面渲染直到它接收了执行了所有的CSS。CSS是渲染阻塞是因为规则可以被覆盖,所以内容不能被渲染直到CSSOM的完成。
CSS有其自身的规则集合用来定义标识。CSS中的C代表的是层叠。CSS规则是级联的。随着解析器转换标识为节点,节点的后代继承了样式。像处理HTML那样的增量处理功能没有被应用到CSS上,因为后续规则可能被之前的所覆盖。CSS对象模型随着CSS的解析而被构建,但是直到完成都不能被用来构建渲染树,因为样式将会被之后的解析所覆盖而不应该被渲染到屏幕上。
从选择器性能的角度,更少的特定选择器是比更多的要快。例如 .foo{}
是比 .bar .foo{}
更快的因为当浏览器发现了 .foo
,接下来必须沿着DOM向上走来检查 .foo
是不是有一个祖先 .bar
。越是具体的标签浏览器就需要更多工作,但这样的弊端未必值得优化
解析CSS的时间,更具体的规则更昂贵是因为它必须遍历更多的DOM树节点,但这所带来的额外的消耗通常是很小的。先测量一下,然后按需优化。在CSS中选择器的性能优化,提升仅仅是毫秒级的。
CSS优化
优化渲染
浏览器有一个固定的渲染流程——只有在布局layout完成后才能绘制paint页面,而布局的前提是要生成渲染树render tree,而渲染树的生成则是需要DOM和CSSOM树的配合。
如果先让用户看到一个没有样式的画面,等CSS样式解析完后再重绘repaint,这样的体验会很差,所以,浏览器会等到需要CSS时才开始渲染,只有在下载完CSS并生成CSS对象模型CSSOM后,浏览器才会绘制页面。
为了优化CSSOM的构建并提高页面性能,可以根据当前CSS的状态执行一下一项或多项操作:
- 删除不必要的样式:没有银弹,尽量保持CSS的模块化,添加或删除的时候谨慎小心
- 将CSS拆分为独立模块:CSS模块化可以延迟加载在页面加载阶段非必要的CSS,缩短初始CSS的阻塞和加载时间,最简单的方法是将CSS拆分为独立的文件,并仅加载所需要内容
link
标签的media
属性可以告诉浏览器何时应用样式表,不会阻塞渲染。 - 最小化和压缩CSS:使用打包工具压缩代码,确保服务器提供例如gzip的压缩
- 简化选择器:简化和降低优先级
- 不要将样式应用于不需要的元素:不要使用通用选择器将样式应用于所有元素,或者至少应用于比实际需要的元素更多的元素。这种类型的样式会对性能产生负面影响,特比是在较大的站点上。
- 使用CSS精灵图减少图像相关的HTTP请求:减少获取图片所需要的HTTP请求数量
- 预加载重要资源:使用
rel="preload"
将link
元素转换为预加载器,用于关键资源,包括CSS/字体/图片
<link rel="preload" href="style.css" as="style" />
<link legacyBehavior rel="preload" href="ComicSans.woff2" as="font" type="font/woff2" crossorigin />
<link legacyBehavior rel="preload" href="bg-image-wide.png" as="image" media="(min-width: 601px)" />
使用 preload
,浏览器会尽快地获取引用的资源,并将其存储在浏览器缓存中,以便在后续代码中引用时更快地使用它们,为了让用户尽可能流畅,应提前加载页面初期用户会遇到的高优先级资源。还可以使用 media
属性创建响应式的预加载器。
处理动画
动画可以改善感知性能,使界面更加流畅,让用户在等待页面加载时感觉到进展。然而更大更多的动画自然需要更多的处理能力来处理,这可能会降低性能。
最简单的建议是减少所有不必要的动画,还可以为用户提供一个控件/站点选项,让他们可以关闭动画。
对于必要的DOM动画,尽可能使用CSS动画,而不是JavaScript动画
选择需要进行动画处理的属性,某些属性在进行动画处理时会触发回流,进一步也会触发重绘,应该避免使用这些属性,这些熟悉包括:
- 修改元素的尺寸,width/height/border/padding
- 重新定位元素,margin/top/bottom/left/right
- 更改元素的布局,align-content/align-items/flex
- 添加改变元素的几何形状的视觉效果,box-shadow
可以的话使用不会引起回流、重绘的属性进行动画处理:
- 变换 Transforms
- backface-visibility
- perspective
- perspective-origin
- transform
- transform-box
- transform-origin
- transform-style
- 透明度 opacity
- 滤镜 filter
在设备GPU上进行动画处理(合成 compositing),转移到主线程外。通过选择特定的动画实现,浏览器会自动将这些动画发送到GPU来处理:
- 3D变换动画,
transform:translateZ()
和rotate3d()
- 具有某些其他属性动画的元素,
position: fixed
- 应用了
will-change
的元素- 浏览器可能会在元素实际发生变化之前进行优化设置,这类优化可以通过提前完成可能需要大量的工作,提高页面的响应速度,CSS的
will-change
属性向浏览器提示元素预期的变化方式 - 应该作为处理现有的性能问题的最后一招
- 浏览器可能会在元素实际发生变化之前进行优化设置,这类优化可以通过提前完成可能需要大量的工作,提高页面的响应速度,CSS的
- 特定的在其自己层中渲染的元素,
video
/canvas
/iframe
在GPU上进行动画处理可以提高性能,尤其是在移动设备上。
优化渲染阻塞
CSS可以使用媒体查询将样式限定在特定条件下。默认情况下,浏览器假设每个指定的样式表都会阻塞渲染,通过添加具有媒体查询的media属性告诉浏览器样式表应该何时应用。当浏览器看到一个样式表时,它会知道只需要将其应用于特定的情况,它仍然会下载样式表,但不会阻塞渲染。通过将CSS拆分为多个文件,主要的阻塞渲染文件会变得更小,从而减少了阻塞的时间。
改善字体性能
选择Web安全字体可以使用更少的字体。字体仅在使用 font-family
属性应用于元素时才会加载,而不是在首次使用 @face-font
at规则引用时加载。使用 rel="preload"
来提交加载重要的字体,这样在实际需要时才可以更快地使用
<link legacyBehavior rel="preload" href="OpenSans-Regular-webfont.woff2" as="font" type="font/woff2" crossorigin />
如果 font-family
声明位于一个大型的外部样式表,并且在解析过程中不会被理解访问到,那么这种做法更有益处。不过要适当权衡,字体文件相当大,如果预加载太多字体,可能会延迟其他资源的加载。
还可以采用:
- 使用
rel="preconnect"
与字体提供方建立早期连接 - 使用CSS字体加载API通过JavaScript自定义字体加载行为
- CSS字体加载API提供了控制和跟踪字体加载过程的能力,运行添加到Document、Worker的字体集中。
只加载所需的字形,限制浏览器需要下载的字形数量,通过创建仅包含所需子集的字体文件来实现,这个过程叫做子集化。使用 @font-face
的 unicode-range
描述符来指定何时使用子集字体,如果页面中没有使用该范围的任何字符,则不会下载该字体。
@font-face {
font-family: 'Open Sans';
src: url('OpenSans-Regular-webfont.woff2') format('woff2');
unicode-range: U+0025-00FF;
}
使用 font-display
描述符定义字体的显示行为,使得文字在字体正在加载或加载失败时都能以备用字体显示,这通过使文本可见而不是显示空白屏幕来提高性能,但代价是出现未样式化文本的闪烁。
使用CSS局限优化样式重新计算,指示浏览器将页面的不同部分隔离开,并独立地优化它们的渲染。这允许在渲染各个部分提高性能。
contain
属性运行作者精确地指定要应用于页面上各个容器的局限类型。这使得浏览器可以针对DOM的一部分重新计算布局、样式、绘制、大小或它们的任意组合。
渲染树
渲染树包括了内容和样式:DOM和CSSOM树结合为渲染树,为了构造渲染树,浏览器检查每个节点,从DOM树的根节点开始,并且决定哪些CSS规则被添加。
渲染树只包含了可见内容,头部通常不包含任何可见的信息,因此不会被包含在渲染树中,如果有元素上有 display:none;
,它本身和其后代都不会出现在渲染树中。
布局
一旦渲染树被构建,布局变成了可能。布局取决于屏幕的尺寸。布局这个步骤决定了在哪里和如何在页面上放置元素,决定了每个元素的宽和高,以及他们之间的相关性。
什么是一个元素的宽,块级元素,根据定义,默认有父级宽度的100%,一个宽度50%的元素,将占据父级宽度的一半,除非另外定义,body有100%的宽,意味着它占据视窗的100%,设备的宽度影响布局
视窗的元标签定义了布局视窗的宽度,从而影响了布局。浏览器使用视窗的默认宽度,默认全屏浏览器是960px。默认情况下手机浏览器的全屏浏览器通过设置 meta name="viewport" content="width=device-width"
, 宽度将会是设备的宽度而不是默认的视窗宽度。设备宽度当用户在横向和纵向模式旋转他们的手机时将会改变,布局发生在每次设备渲染或浏览器缩放时。
布局性能受DOM影响——节点数越多,布局就需要更长的时间。布局将会变成瓶颈,如果期间需要滚动或者其他动画将会导致迟滞。20ms的延迟在加载或者方向改变时或许还可以接受,但在动画或者滚动时就会迟滞。任何渲染树改变的时候,像添加节点、改变内容或者在一个节点更新盒模型的时候布局就会发生。
为了减少布局事件频率和时长,批量更新或者避免改动盒模型属性。
绘制
最后一步是将像素绘制到屏幕上。一旦渲染树创建并且布局完成,像素就可以被绘制到屏幕上。加载时,整个屏幕被绘制出来。之后,只有受影响的屏幕区域会被重绘,浏览器被优化为只重绘需要绘制的最小区域。绘制时间取决于何种类型的更新被附加到渲染树上。绘制是一个非常快的过程,所以聚焦在提升性能时这大概不是最有效的部分,重点要记住的是当测量一个动画帧需要的时间需要考虑到的布局和重绘时间。添加到节点的样式会增加渲染时间,但是移除样式增加的0.001ms或许不能让你的优化物有所值,记住先测量,然后可决定它的优化优先级。
优化CRP
提升页面加载速度需要通过被加载资源的优先级、控制它们加载的速度和减小这些资源的体积。性能提示包含:
- 通过异步、延迟加载或者消除非关键资源来减少关键资源的请求数量
- 优化必须的请求数量和每个请求文件的体积
- 通过区分关键资源的优先级来优化被加载关键资源的顺序,来缩短关键路径长度
CSS动画与JavaScript动画的性能
CSS过渡和动画
CSS的 transition
和 animation
可以用于编写动画,都有各自的使用场景:
transition
提供一个简单的方法去创造当前样式与结束样式之间的动画。尽管一个元素处于过渡状态中,新的过渡动画也会立即从当前样式开始,而不是直接跳转到CSS的最终状态。animation
通过一个初始状态属性值集合与最终状态属性值集合创造动画,而不是单单的初始和最终状态。由两个部分组成:描述CSS动画的样式,以及一组关键帧,表示动画样式的开始和结束状态,以及可能的中间状态。
就性能方面来说,无论通过CSS animation还是Transition创造动画,都没有区别,这两者都归类与基于CSS的动画。
requestAnimationFrame
提供了一种用JavaScript代码制作动画的高效方式,在方法回调函数中通过简单改变元素的样式或更新画布会是来创建动画。
跟CSS transitions和animations一样,当页面在后台运行时候,requestAnimationFrame会暂停
浏览器可以优化渲染流程,尽可能使用CSS transition
和 animation
创建动画,如果动画很复杂,就不得不依赖于JavaScript动画。
优化启动性能
尽可能异步地启动,不要将所有的启动代码在应用主线程中的唯一一个事件处理函数中运行。在后台线程创建一个Web worker,做尽可能多的工作,例如获取和处理数据。然后必须在主线程完成的事情,用户事件和渲染用户界面应该被分成小的片段,这样当应用启动时,应用的事件循环就可以持续运行下去,可以避免应用、浏览器以及/或者设备出现锁死。
异步化
- 启动时,在需要异步执行的脚本标签上使用
defer
或async
属性,也会运行HTML解析器更高效处理文档 - 解码资源文件,最好在workers里实现
- 处理浏览器支持的数据格式时,使用设备或者浏览器内置的解码器而不是运行自己的。预先提供的那个基本上一定会快得很多,并且能够减少应用启动体积,另外,浏览器可以自行并行化这些解码器的工作
- 所有能并行的数据处理应该并行化,不要一团接一团处理数据,同时处理它们
- 启动的HTML文件中,不要包含不会在关键路径下出现的脚本或样式表。只在需时加载他们
- 不要强迫Web引擎构建不需要的DOM, 一种简单的hack方式就是把HTML留在文档中,但是在外层包裹注释
- 当文档的一部分需要被渲染,加载被注释的HTML
使用dns-prefetch
尝试在请求资源之前解析域名,这可能是后面要加载的文件,也可能是用户尝试打开的链接目标。
当浏览器从服务器请求资源时,必须先将该跨域域名解析为IP地址,然后浏览器才能发出请求,此过程称为DNS解析。虽然DNS缓存可以帮助减少延迟,但DNS解析可能会给请求增加明显的延迟。对于打开了与许多第三方的连接的网站,此延迟可能会大大降低加载性能。
dns-prefetch
可帮助开发人员掩盖DNS解析延迟。
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
dns-prefetch
仅对跨源域上的DNS查找有效,因此避免使用它指向自己的站点或域,当浏览器看到提示,站点背后的IP已经被解析- 可以通过
HTTP Link
将dns-fetch
以及其他资源提示指定为HTTP标头之一 dns-prefetch
与preconnect
提示配对。dns-prefetch
执行DNS查询,preconnect
建立与服务器的连接。这个过程包括DNS解析,以及建立TCP连接,如果是HTTPS网站,就进一步执行TLS握手。这两者结合起来,可以进一步减少跨源请求的延迟感知
<link legacyBehavior rel="preconnect" href="https://fonts.googleapis.com/" crossorigin />
<link rel="dns-prefetch" href="https://fonts.googleapis.com" />
如果页面需要建立与许多第三方域的连接,则将它们预先连接则会适得其反。
preconnect
提示最好仅用于最关键的链接。对于其他连接,只需使用dns-prefecth即可以节省第一步——DNS查询的时间