web渲染引擎以及前端渲染性能的优化
最近在做行业地图的时候,也在思考渲染性能的问题,于是今天就想和大家聊聊这个主题,大家可以帮我补充一下。
一、Why
性能优化一直以来都是我们前端中的一个重要部分,项目设计得再好,功能实现了,但是可用性不高是很大的问题。项目实战中还是会遇到性能问题的瓶颈,前端是项目开发的最后一公里,性能问题会直接影响到用户体验。
目前大家熟悉的优化web性能包括下面几个:
1是缓存包括本地缓存,内存缓存等,而其中 Service Worker + Cache API的缓存技术(可实现离线可用的“秒开”应用),如果有相关场景的业务大家可以用这种方式。
2是网络请求的性能优化,包括要减少重定向,实现并行请求,做DNS预解析等等。
并行请求:链接:juejin.cn/post/703264…
让一个 TCP 连接可以传输多个请求和响应,但是这些请求和响应是串行的。在需要请求的资源较多,且资源之间没有依赖关系时(比如十个互不依赖的 CSS 文件),我们更希望这十个请求是并行的:十个请求同时(也可以短时间内依次)发出,十个响应陆续返回。这样一来,下载速度不就是原来是十倍嘛!
优点:并行连接可以在带宽资源充足的情况下同时建立多个HTTP连接,加快页面的加载速度。
缺点:并行连接在带宽资源不足的情况下会是连接竞争资源,效率反而下降。同时建立多条连接会消耗大量内存,对服务器来说,大量的用户产生大量的连接可能会超过服务器的处理能力,所以服务器一般能够关闭来自特定客户端的超量连接!
因此浏览器们不约而同地限定了每个域名的并行连接数量的上限:6 个。也有些浏览器将其设置为 2、4、8、9、10、12 等。但这难不倒我们,既然每个域名最多有 6 个连接,那我们多用几个域名不就可以突破限制了吗?比如:
- css.cdn.com 专门用于下载 CSS 文件
- js.cdn.com 专门用于下载 JS 文件
- image.cdn.com 专门用于下载图片文件
- juejin.cn 专门用于下载 HTML 文件
这样一来,并行连接数的上限就可以扩大到 24 个了。 但是浏览器为了防止程序员滥用这个特性,又加了另一个限制:整个浏览器的并发连接数也有上限。
3是页面解析与处理时的优化,
4是静态资源的优化和缓存,包括图片、字体、js包等。
今天主要针对前端体验相关的渲染性能的问题。
二、How
那么浏览器在渲染页面的时候,到底做了啥,以及性能的尺子performance。
- 浏览器的主要组件可分为如下几个部分:
- 界面控件 – 包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分
- 浏览器引擎 – 只有一个,主控整个系统的运行,管理大部分的日常事务
- 渲染引擎 – 负责显示请求的内容。比如请求到HTML, 它会负责解析HTML、CSS并将结果显示到窗口中
- 网络 –
- UI后端 –它提供平台无关的接口,内部是使用操作系统的相应实现
- JS解释器
- 存储数据的数据存储持久层
- 浏览器的进程/线程
见《前端其他技术/底层技术/浏览器进程.md》
今天的重点在于这个渲染引擎。
浏览器呈现网页这个过程,就像一个黑盒。在这个神秘的黑盒中,有许多功能模块,内核内部的实现正是很多功能模块相互配合协同工作进行的。
- 解析HTML 首先是解析HTML/SVG/XHTML,这个过程主要是把这些文档解析为 DOM 树。 但如果遇到 标签会停止解析(无defer/async),先执行标签当中 JavaScript;如果是外联方式,也需要等待下载并且执行完对应的 JavaScript 代码,然后才能够继续执行解析 HTML 的工作。HTML解析完成后触发 DOMContentLoaded 事件,这里我们就可以操作 DOM了。
- 解析CSS 。解析 CSS 遇到 标签,会阻塞 CSS 的解析。CSS 解析器将 CSS 解析成 css树 (也被叫做 CSSOM 树),它和 DOM 树结构类似。解析对应关系如下
- 计算样式 CSSOM 与 DOM 树 结合,生成页面 render 树。其中DOM解析和CSS解析是两个并行的进程
- 【翻页】后面的几个步骤可以概括为上面这个图
- 传统做法,
<script>元素都应该放在页面的<head>元素中。(这种做法的目的是把所有的外部文件引用都放在一起,例如 CSS文件和Javascript文件)- 但是,放在
<head>元素中,就意味着必须等到全部的javascript代码都被下载、解析和执行完成以后,才能开始呈现页面内容。 (因为浏览器在遇到<body>标签时才开始呈现内容)- 因此,放在
<head>元素中,容易出现较长时间的空白页面(因为呈现被阻塞,如果javascript代码需要很多的话)- 所以,最终
<script>元素一般都放在<body>标签中页面内容的后面(也就是</body>标签的前面)link标签需要放在head标签里,是因为不希望页面先渲染出不带样式的结果后再渲染出正确的结果。
渲染过程概括
在编写web页面时,我们写的页面代码是如何被转换成屏幕上显示的像素的。这个转换过程可以归纳为这样的一个流水线,包含五个关键步骤:
-
咱们刚刚讲完生成渲染树之后,这一步结束之后,就确定了每个DOM元素上该应用什么CSS样式规则。
-
到了Layerout这一步,是具体计算每个DOM元素最终在屏幕上显示的大小和位置。web页面中元素的布局是相对的,因此一个元素的布局发生变化,会联动地引发其他元素的布局发生变化。比如,元素的宽度的变化会影响其子元素的宽度,其子元素宽度的变化也会继续对其孙子元素产生影响。因此对于浏览器来说,布局过程是经常发生的。咱们经常说的回流就是指的layerout这一步。
-
然后就是paint绘制了。它就是填充像素的过程。包括绘制文字、颜色、图像、边框等,也就是一个DOM元素所有的可视效果。一般来说,这个绘制过程是在多个层上完成的。重绘(repaint)
-
最后就是composite渲染层合并。由于绘制的时候是由多个层完成的,绘制过程全部完成后,浏览器会将所有层按照合理的顺序合并成一个图层,然后通过显卡显示在屏幕上。对于有位置重叠的元素的页面。
-
注意composite概念,浏览器渲染的图层一般包含两大类:普通图层以及复合图层。
-
普通文档流内可以理解为一个默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中**,哪怕是absolute布局(fixed也一样),即使脱离普通文档流,但它仍然属于默认复合层)
-
可以通过硬件加速的方式—GPU,声明一个新的复合图层(最常用的方式:translate3d、translateZ),它会单独分配资源,会脱离普通图层,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘。
- GPU中,各个复合图层是单独绘制的,所以互不影响
-
-
总结一下composite的步骤:
-
第一步:计算。主线程会计算每个Graphics Layers的合成时所需要的数据,包括位移(Translation)、缩放(Scale)、旋转(Rotation)、Alpha 混合等操作的参数。
-
试想一下,如果给你一张纸,让你先把纸的背景涂成蓝色,然后在中间位置画一个红色的圆,最后再在圆上画个绿色三角形。你会怎么操作呢?
通常,你会把你的绘制操作分解为三步:
-
绘制蓝色背景;
-
在中间绘制一个红色的圆;
-
再在圆上绘制绿色三角形。
-
-
-
- 第二步:commit,主线程会通知合成器线程同步layer tree的信息。**当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程**。绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。你可以结合下图来看下渲染主线程和合成线程之间的关系。
合成线程提交图块给栅格化线程池
- 第三步是光栅化(Rasterization)。合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。 合成器线程会创建出若干个Compositor Tile Worker Thread,并将纹理写入GPU内存中, 执行绘制操作,并填充纹理(texture)。
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。相信你还记得,GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。
【这里咱们可以用performance的layers来看,后面会一起讲到】
但是上面的五个步骤顺序不是依次进行的,如果说是一次进行,就会发生下面这个图的现象,也就是页面会等到所有都解析完,才开始渲染,就会造成很不友好的体验,像下图中最底的那一行图片。
为了在浏览器上能渐进式地展现出来,在第一次加载的时候能让一些重要的元素尽快显示出来,所以实际上这个流水线的过程存在一定交叉的。
为了能渐进式地展现出来关键在于,html在构建dom树的时候,可以先构建一部分,而且css解析的时候会阻塞页面的渲染【即css解析不影响DOM的解析,但是会影响render树】,试想一下,如果一开始设置了20px,后面又被覆盖成50px,就会造成先小后大的视觉了。
上述过程的每一步中都有发生性能卡顿的可能,因此一定要弄清楚你的代码将会运行在哪一步。
咱们可以实际来看一下这些过程。首先咱们要准备好一份代码一个尺子。
左边是写的一个简单的布局,咱们可以先忽略动画,右边我们用performance来记录渲染过程。打开reload来记录。
咱们一开始提到渲染过程,包括了HTML解析,layout、 paint、 composite过程。首先task任务下有一个parseHtml的过程,这个过程包含了dom的解析和css的解析和js事件的执行,这个过程可以看到有一个DCL事件的声明周期,这个之前提到过是x xx, dom在解析之后会触发的事件。
性能指标
讲过了浏览器的工作机制,咱们来看看常见的性能的踩坑。
首屏渲染/DCL等
FP (First Paint) 首次绘制白屏时间,FCP (First Contentful Paint) 首次内容绘制首屏时间LCP (Largest Contentful Paint)最大内容渲染 用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。LCP 其实能比前两个指标更能体现一个页面的性能好坏程度,因为这个指标会持续更新。举个例子:当页面出现骨架屏或者 Loading 动画时 FCP 其实已经被记录下来了,但是此时用户希望看到的内容其实并未呈现,我们更想知道的是页面主要的内容是何时呈现出来的。 LCP 指标是能够帮助我们实现想要的需求的。官方推荐的时间区间,FP 及 FCP 两指标在 2 秒内完成的话我们的页面就算体验优秀。LCP在 2.5 秒内表示体验优秀。
FID(First Input Delay)首次输入延迟,记录在 FCP 和 TTI 之间用户首次与页面交互时响应的延迟。
这个指标其实挺好理解,就是看用户交互事件触发到页面响应中间耗时多少,如果其中有长任务发生的话那么势必会造成响应时间变长。
CLS(Cumulative Layout Shift)
累计位移偏移,记录了页面上非预期的位移波动。
大家想必遇到过这类情况:页面渲染过程中突然插入一张巨大的图片或者说点击了某个按钮突然动态插入了一块内容等等相当影响用户体验的网站。这个指标就是为这种情况而生的,计算方式为:位移影响的面积 * 位移距离。CLS 推荐值为低于 0.1,越低说明页面跳来跳去的情况就越少,用户体验越好。
Google 在今年五月提出了网站用户体验的三大核心指标,分别为:
- LCP
- FID
- CLS
LCP 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但是上文也说过 LCP 能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
FID 代表了页面的交互体验指标,毕竟没有一个用户希望触发交互以后页面的反馈很迟缓,交互响应的快会让用户觉得网页挺流畅。
CLS 代表了页面的稳定指标,尤其在手机上这个指标更为重要。因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做的很差。
三、Oh, no!
虽然我们都对渲染引擎大体有所了解,在应用时也可能踩到一些性能的“坑”。下面就让我们来具体看一看。
咱们可以从一开始讲到的渲染过程看看,哪些路有坑。
1. 首先是DOM的解析过程中SVG的渲染。
项目中咱们经常因svg保真的优势采用svg,对于小logo和图标和来说,SVG是很理想的。DOM的解析除了HTML,也包括了SVG。而且SVG在渲染的时候需要比像素图更多的计算能力,这也就意味着性能的损耗。如果你的logo是特别复杂的,svg树的结构很复杂,它可能会很耗费性能,甚至文件大小也非常大。 所以当页面上有很多图标时,要慎用复杂的svg,当然也有对svg进行优化的几个方法,这里就不细讲了。
2. 关于静态资源的加载问题。
咱们经常说要减少http请求,其中一个关键的原因是,页面请求资源时,浏览器会同时和服务器建立多个TCP连接,而Chrome浏览器最多允许对同一个域名建立6个TCP连接。那么这里有一个问题,tcp能同时发送多少请求呢,一般情况下,http1.1单个TCP连接,在同一时间只能处理一个http请求,HTTP2 由于提供了多路传输功能,多个http请求可以同时在同一个TCP连接中进行传输。
超过这一限制的后续请求将会被阻塞,就是后面的资源都需要排队加载。。咱们可以看看一个例子。
咱们来实际看一个很简单的demo,页面上只有10个图片请求,其中有6个图片来自同一域名且是http1.1请求。。先用network3g来看一下,可以看出在第七个资源有一段事件是在pending状态的,再 用performance 可以更直观地看出加载过程,前面 6个统一域名已经达到了6个上限,而第7个资源只能等到有资源释放才可以开始加载。
所以当页面加载时有多个静态资源请求的小伙伴们要注意了:
当首屏大且多的静态资源会不会阻塞了js资源的加载。那么怎么解决呢?
\1. 减少首次渲染时静态资源的加载(如懒加载/雪碧图等)
\2. 升级到HTTP2或者是资源放到不同域名下
\3. 缓存(比如前面提到的service work + cache API)的方案
4. 降低样式计算的复杂度
咱们回到前面的渲染过程的图,layout层主要在渲染树上运行布局以计算每个节点的几何,添加或移除一个DOM元素、修改元素属性和样式类、应用动画效果等操作,都会引起DOM结构的改变,从而导致浏览器需要重新计算每个元素的样式、对页面或其一部分重新布局(多数情况下)。那么降低样式计算的复杂度也是优化的点之一。主要有两个优化的方向点,首先是样式选择器的复杂度。
计算样式中根据匹配的样式选择器来获取对应的具体样式规则,
计算样式的第一步是创建一套匹配的样式选择器,浏览器就是靠它们来对一个元素应用样式的。
对于第一个样式选择器,浏览器先确定做了哪些事情呢? 首先是确定这个元素有没有title的class属性,它是不是还有个父亲,而且它正好是倒数-n+1个元素。这个过程就增加了它的计算链路。而像下面那个加一个className的选择器,效率会更高。BEM是一个很好的选择,不管是在代码结构、还是样式查找速度方面,它的表现都是很棒的。
目前,浏览器他们会对每个DOM元素维护一个独有的样式规则小集合,如果这个集合发生改变,才重新计算该元素的样式。所以我们应该尽可能减少需要执行样式计算的元素的个数,例如,如果需要对某个元素进行offset的修改,那么应尽量减少其子元素和集合其他元素的总个数。
5. 减少回流和重绘的操作
说到渲染性能,重绘和回流的问题是很需要关注的点。咱们回到一开始渲染过程的图。之前提到渲染关键流程可以概括为5个步骤,但是不意味着页面每一帧的渲染都需要经过上述五个步骤的处理。实际上,每一帧的渲染可能有另外两种 常用的 流水线。
第2个流水线中。如果你修改一个DOM元素的具有只触发重绘的属性,比如背景图片、文字颜色或阴影等,这些属性不会影响页面的布局,因此浏览器会在完成样式计算之后,跳过布局过程,只做绘制和渲染层合并过程。
第三种;如果你修改一个不会触发回流和重绘的CSS属性,那么浏览器会在完成样式计算之后,跳过前面两个过程,直接做渲染层合并。
DOM 树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。那么第三种方式在性能上是最理想的,某些情况下,对于动画和滚动这种渲染,咱们可以通过一些属性实现第三种渲染流程。
硬件加速时请使用index
使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染
具体的原理时这样的: webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低, 那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的), 会默认变为复合层渲染,如果处理不当会极大的影响性能
简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意。
我们偶然发现隐式合成比你想象的更频繁。 浏览器会将元素提升为合成层的原因有很多,其中包括:
- 3D transforms:
translate3d,translateZ等等; <video>,<canvas>和<iframe>元素;- 通过
Element.animate()而有transform动画和opacity属性的元素; - 通过СSS transitions 和 animations而有
transform动画和opacity属性的元素; position: fixed;will-change;filter;- 等等
但只有下面才能开启
-
3D transforms:translate3d、translateZ 等
-
video、canvas、iframe 等元素. 【 =>>>不对,不能提升】
-
通过 Element.animate() 实现的 opacity 动画转换
-
通过 СSS 动画实现的 opacity 动画转换
-
position: fixed. 【 =>>>不对,不能提升】
-
具有 will-change 属性
-
will-change: transform; will-change: opacity;
-
-
对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition
【利用will-change 属性,将CPU 消耗高的渲染元素提升为一个新的合成层,才能开启GPU 加速的】
filter: blur(100px);
这行 CSS 代码用于实现一个高斯模糊,来构造一个优惠券模块的底部阴影。由于活动配置了多个优惠券,导致页面里存在多个设置了这个属性的 div 元素,而 IOS 手机的浏览器似乎对这个属性的渲染十分吃力(然而为何吃力的原因不得而知),进而导致渲染进程的 CPU 占用率过高,最终造成卡顿。
哦?CPU 忙不过来了?好办嘛!我给优惠券模块又加了这样一行代码,然后问题迎刃而解 ......
will-change: transform;
那就是利用GPU 加速 使用渲染层合并属性,包括利用transform和opacity来实现CSS动画,使用js动画使用 will-change: transform, opacity 或 translateZ 提升移动的元素。
在这个过程,浏览器会提前更新CSS属性,提前告诉GPU动画如何开始和结束及所需要的指令* *,页面所有的复合层发送给GPU,作为图像缓存。 如果浏览器没有看到任何会导致重排或重绘的属性,动画的发生仅仅是复合层间相对移动。
咱们来看一个简单的left的例子。当我们采用left来触发动画时,从performance中可以看到,layer图层只有一个,但当我们使用translateZ时,就会讲这一层提升到一个复合层。
5. 避免强制同步布局事件的发生
- 避免强制同步布局和布局抖动;先统一读取样式值,然后统一进行样式更改。
当你修改了元素的样式属性之后,浏览器会将会检查为了使这个修改生效是否需要重新计算布局以及更新渲染树。对于DOM元素的“几何属性”的修改,比如width/height/left/top等,都需要重新计算布局。
首先是执行JavaScript脚本,然后是样式计算,然后是布局。但是,我们还可以强制浏览器在执行JavaScript脚本之前先执行布局过程,这就是所谓的强制同步布局。
在 JavaScript 运行时,来自上一帧的所有旧布局值是已知的。
function logBoxHeight() {
box.classList.add('super-big');
// Gets the height of the box in pixels
// and logs it out.
console.log(box.offsetHeight);
}
上面的例子中,为了获取元素box的高度,浏览器必须先应用样式更改(因为增加了 super-big 类),然后运行布局。这时,box.offsetHeight 才能返回正确的高度。但是这是不必要的,并且可能是开销很大的工作。
上面的例子中,代码首先修改了一个元素的样式,接下来读取另一个元素的clientHeight属性,由于之前的修改导致当前DOM被标记为脏,为了保证能准确的获取这个offsetHeight属性,浏览器会进行一次layout。
正确写法应为:
function logBoxHeight() {
// Gets the height of the box in pixels
// and logs it out.
console.log(box.offsetHeight);
box.classList.add('super-big');
}
有这么一种常见,想要来改变:
一个可能会引起你惊讶的消息是,当你获取请求DOM的一些值的时候也会立即引起reflow。为了避免触发不必要的布局过程,你应该首先批量读取元素样式属性,然后再对样式属性进行写操作。
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
IE中的 getComputedStyle(), 或 currentStyle
function resizeWidth() {
// 会让浏览器陷入'读写读写'循环
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = box.offsetWidth + 'px';
}
}
// 改善后方案
var width = box.offsetWidth;
function resizeWidth() {
for (var i = 0; i < paragraphs.length; i++) {
paragraphs[i].style.width = width + 'px';
}
}
// 原文链接:https://blog.csdn.net/xcg132566/article/details/108004965
另一个例子:
// 优化前
// Read
var h1 = element1.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
// Read (triggers layout)
var h2 = element2.clientHeight;
// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';
// Read (triggers layout)
var h3 = element3.clientHeight;
// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';
// 优化后:
// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
优化前:
优化后:
上面提到的一个批量读写是一个,主要是因为获取一个需要计算的属性值导致的,那么哪些值是需要计算的呢?可查: gist.github.com/paulirish/5…
6. 长列表的优化
zhuanlan.zhihu.com/p/414128954
对于渲染大量数据,业界常用的做法是两种:时间分片和虚拟列表【即根据可视窗口展示】。这里,我们先来看下时间分片的做法。时间分片,简单点理解就是把数据拆成很多份,分批地来进行渲染。利用createDocumentFragment进行数据分批,利用requestAnimationFrame来分时间渲染。
每批的数据来说,我们不用一个一个 li 节点插入到DOM节点,而是可以使用DocumentFragment 来将一批的 li 节点一次性插入到DOM。
requestAnimationFrame 和createDocumentFragment的使用。
createDocumentFragment
新增、删除、修改、查找节点时,对同一个节点需多次插入新的dom节点时,可以使用文档片段documentFragment,先插入到文档片段中,再把文档片段一次性插入到指定节点中,因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流。因此,使用文档片段通常会带来更好的性能。
参考:www.jianshu.com/p/8ae83364c…
首先,让我们看几种常见的动态创建html节点的方法,如下所示:
| 方法 | 说明 |
|---|---|
| createAttribute(name) | 用指定名称name创建特性节点 |
| createComment(text) | 创建带文本text的注释节点 |
| createDocumentFragment() | 创建文档碎片节点 |
| createElement(tagname) | 创建标签名为tagname的节点 |
| createTextNode(text) | 创建包含文本text的文本节点 |
以上这些方法,每次JavaScript对DOM的操作都会改变当前页面的呈现,并重新刷新整个页面,从而消耗了大量的时间。而createDocumentFragment()的作用,就是可以创建一个文档碎片,把所有的新节点附加其上,然后把文档碎片的内容一次性添加到document中。
DocumentFragment: 表示文档的一部分(或一段),更确切地说,它表示一个或多个邻接的 Document 节点和它们的所有子孙节点。且不属于文档树,继承的 parentNode 属性总是 null。 不过它有一种特殊的行为,该行为使得它非常有用,即当请求把一个DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。 这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。 它还有利于实现文档的剪切、复制和粘贴操作。 可以用Document.createDocumentFragment() 方法创建新的空DocumentFragment 节点。
使用appendChild逐个向DOM文档中添加1000个新节点:
for (var i = 0; i < 1000; i++)
{
var el = document.createElement('p');
el.innerHTML = i;
document.body.appendChild(el); //直接用appendChild向文档中插入节点
}
使用createDocumentFragment()一次性向DOM文档中添加1000个新节点:
var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++)
{
var el = document.createElement('p');
el.innerHTML = i;
frag.appendChild(el); //首先将新节点先添加到DocumentFragment 节点
}
document.body.appendChild(frag);//然后用appendChild插入文档中
总结:我们可以把DocumentFragment当成占位符。
我们可以理解为DocumentFragment (文档碎片节点)是一个插入结点时的过渡,我们把要插入的结点先放到这个文档碎片里面,然后再一次性插入文档中,这样就减少了页面渲染DOM元素的次数。经IE和FireFox下测试,在append1000个元素时,效率能提高10%-30%,FireFox下提升较为明显。 不要小瞧这10%-30%,效率的提高是着眼于多个细节的,如果我们能在很多地方都能让程序运行速度提高10%-30%,那将是一个质的飞跃!
window.requestAnimationFrame
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
js 动画的渲染性能来说,不要再16ms多次触发重复渲染,一帧就是16ms,如果使用setTimeout和setInterval无法保证callback函数的执行时机,很可能在帧结束的时候执行,从而导致丢帧。可以使用requestAnimationFrame来使用。
requestAnimationFrame还可以DocumentFragment结合一个实现一个超长列表的渲染。
<html>
<head>
<meta charset="UTF-8" />
<title>improved</title>
<style>
* {
margin: 0;
padding: 0;
}
ul,
li {
list-style: none;
}
.box {
width: 1050px;
margin: 100px auto;
}
ul li {
width: 600px;
height: 80px;
color: #000;
font-size: 20px;
}
</style>
</head>
<body>
<div class="box">
<button id="button">开启</button>
<ul id="ul"></ul>
</div>
<script type="text/javascript">
/*
* createDocumentFragmen & requestAnimationFrame
*/
const total = 100000;
const once = 30;
const loopTimes = Math.ceil(total / once);
const ul = document.getElementById("ul");
let curTime = 0;
ul.innerHTML = createList();
// document.querySelector("#button").addEventListener("click", () => {
// });
function createList() {
const colorMap = ["#f99", "#9ff", "#f9f"];
let mainStr = "";
const fragment = document.createDocumentFragment();
for (let i = 0; i < once; i++) {
const color = colorMap[i % 3];
const li = document.createElement("li");
li.innerHTML = i;
li.style.background = color
fragment.appendChild(li);
}
ul.appendChild(fragment);
curTime++;
loop();
return mainStr;
}
function loop() {
if (curTime < loopTimes) {
requestAnimationFrame(createList);
}
}
</script>
</body>
</html>
可以理解为DocumentFragment (文档碎片节点)是一个插入结点时的过渡,我们把要插入的结点先放到这个文档碎片里面,然后再一次性插入文档中,这样就减少了页面渲染DOM元素的次数。
除了上述讲到的之外,还有很多其他的,例如Web Workers,可以把纯计算工作放到
7. 回流必定会触发重绘,重绘不一定会触发回流。重绘的开销较小,回流的代价较高。
Over
性能优化,是个减法艺术,项目中碰到性能的问题也很常有,如果碰到了,可以去了解每一个流水线的步骤,用起performance来进行性能分析,会帮我们很多。
性能优化:
该项措施可以帮助我们优化 FP、FCP、LCP 指标。
- 压缩文件、使用 Tree-shaking 删除无用代码
- 服务端配置 Gzip 进一步再压缩文件体积
- 资源按需加载
- 通过 Chrome DevTools 分析首屏不需要使用的 CSS 文件,以此来精简 CSS
- 内联关键的 CSS 代码
- 使用 CDN 加载资源及
dns-prefetch预解析 DNS 的 IP 地址 - 对资源使用
preconnect,以便预先进行 IP 解析、TCP 握手、TLS 握手 - 缓存文件,对首屏数据做离线缓存
- 图片优化,包括:用 CSS 代替蹄片、裁剪适配屏幕的图片大小、小图使用 base64 或者 PNG 格式、支持 WebP 就尽量使用 WebP、渐进式加载图片
- 所以当页面上有很多图标时,要慎用复杂的svg,当然也有对svg进行优化的几个方法,这里就不细讲了。
zhuanlan.zhihu.com/p/37933807?…
-
加载1000条数据:
-
fragment
-
requestAnimationFrame
解决浏览器当前最大的连接数限制问题:
总结:
\1. 首先是DOM的解析过程中SVG的渲染。项目中咱们经常因svg保真的优势采用svg,对于小logo和图标和来说,SVG是很理想的。DOM的解析除了HTML,也包括了SVG。而且SVG在渲染的时候需要比像素图更多的计算能力,这也就意味着性能的损耗。
\2. 关于静态资源的加载问题。Chrome浏览器最多允许对同一个域名建立6个TCP连接。
\1. 减少首次渲染时静态资源的加载(如懒加载/雪碧图等)
\2. 升级到HTTP2(多路复用,能够让一个tcp请求能够并行请求多个;http1.x时,是)或者是资源放到不同域名下
\3. 缓存(比如前面提到的service work + cache API)的方案
\4. 降低样式计算的复杂度【render树】
\5. 减少回流和重绘的操作
如果你修改一个DOM元素的具有只触发重绘的属性,比如背景图片、文字颜色或阴影等,这些属性不会影响页面的布局,因此浏览器会在完成样式计算之后,跳过布局过程,只做绘制和渲染层合并过程。
第三种;如果你修改一个不会触发回流和重绘的CSS属性,那么浏览器会在完成样式计算之后,跳过前面两个过程,直接做渲染层合并。
DOM 树里的每个结点都会有reflow方法,一个结点的reflow很有可能导致子结点,甚至父点以及同级结点的reflow。那么第三种方式在性能上是最理想的,某些情况下,对于动画和滚动这种渲染,咱们可以通过一些属性实现第三种渲染流程。
- 最常用的方式:translate3d、translateZ
- opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
- will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),
作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)
-
-
其它,譬如以前的flash插件
硬件加速时请使用index
使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染
具体的原理时这样的: webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低, 那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的), 会默认变为复合层渲染,如果处理不当会极大的影响性能
简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意
- 常见的内存泄漏场景有哪些?
这里列举了一些常见的内存泄漏场景,遇到内存泄漏问题时可以先自查一遍常见场景,个人感觉能解决日常开发中遇到的90%内存泄漏
console导致的内存泄漏 因为打印后的对象需要支持在控制台上查看,所以传递给console.log方法的对象是不能被垃圾回收的。我们需要避免在生产环境用console打印对象。
框架配合第三方库使用时,没有及时执行销毁 这点可以参考vue cookbook里的例子:避免内存泄漏 — Vue.js 中文文档
被遗忘的定时器 例如在组件初始化的时候设置了setInterval,那么在组件销毁之前记得调用clearInterval方法取消定时器。
没有正确移除事件监听器(各种EventBus, dom事件监听等) 这应该是最容易犯的一个错误,无论新手老手都有可能栽在这里。 特征:performance里,监听器数量会持续上升
面试官:SPA首屏加载速度慢的怎么解决?
性能监控平台
白屏时间FP怎么计算
还有一个比较重要的时间就是白屏时间,它指从输入网址,到页面开始显示内容的时间。
将以下脚本放在 </head> 前面就能获取白屏时间。
<script>
whiteScreen = new Date() - performance.timing.navigationStart
// 通过 domLoading 和 navigationStart 也可以
whiteScreen = performance.timing.domLoading - performance.timing.navigationStart
</script>
错误数据采集
- 资源加载错误,通过
addEventListener('error', callback, true)在捕获阶段捕捉资源加载失败错误。 - js 执行错误,通过
window.onerror捕捉 js 错误。 - promise 错误,通过
addEventListener('unhandledrejection', callback)捕捉 promise 错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。
性能数据上报
性能数据可以在页面加载完之后上报,尽量不要对页面性能造成影响。requestIdleCallback
window.onload = () => {
// 在浏览器空闲时间获取性能及资源信息
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
if (window.requestIdleCallback) {
window.requestIdleCallback(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
})
} else {
setTimeout(() => {
monitor.performance = getPerformance()
monitor.resources = getResources()
}, 0)
}
}
错误收集上报时的问题
XHR同步请求会阻碍页面卸载,如果是刷新/跳转页面的话,页面重新展示速度会变慢,导致性能问题。即,使用的是axios来发送请求,请求发出去了,但是被取消了,服务器那边根本没有收到请求。
毕竟向网络发送请求并获得响应可能会超级慢,有可能是用户网络环境比较差,又或者是服务器挂了,请求一直没返回回来...
基于性能问题,大佬们推荐使用Beacon代替XHR,然后经过一番搜索...
- Beacon API用于将少量数据通过post请求发送到服务器。
Beacon是非阻塞请求,不需要响应- 浏览器将
Beacon请求排队让它在空闲的时候执行并立即返回控制 - 它在
unload状态下也可以异步发送,不阻塞页面刷新/跳转等操作。
navigator.sendBeacon: 这个方法主要用于满足统计和诊断代码的需要,这些代码通常尝试在卸载(unload)文档之前向web服务器发送数据。过早的发送数据可能导致错过收集数据的机会。然而,对于开发者来说保证在文档卸载期间发送数据一直是一个困难。因为用户代理通常会忽略在 unload (en-US) 事件处理器中产生的异步 XMLHttpRequest。
navigator.sendBeacon(url, data);
它主要用于将统计数据发送到 Web 服务器,同时避免了用传统技术(如:XMLHttpRequest)发送分析数据的一些问题。
DNS
- DNS是什么,
- DNS预解析
浏览器DNS解析大多时候较快,且会缓存常用域名的解析值,但是如果网站涉及多域名,在对每一个域名访问时都需要先解析出IP地址,而我们希望在跳转或者请求其他域名资源时尽量快,则可以开启域名预解析,浏览器会在空闲时提前解析声明需要预解析的。如下:
默认情况下浏览器会对页面中和当前域名(正在浏览网页的域名)不在同一个域的域名进行预获取,并且缓存结果,这就是隐式的 DNS Prefetch。其中 Chrome 和 Firefox 3.5+ 内置了 DNS Prefetching 技术并对DNS预解析做了相应优化设置。所以 即使不设置此属性,Chrome 和 Firefox 3.5+ 也能自动在后台进行预解析 。
DNS Prefetch 应该尽量的放在网页的前面,推荐放在 后面。具体使用方法如下:
X-DNS-Prefetch-Control: on // 即开启,一般浏览器都是不需要设置此属性,默认开启的。见上面
X-DNS-Prefetch-Control: off // 关闭
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.img.com">
<link rel="dns-prefetch" href="//www.api.com">
<link rel="dns-prefetch" href="//www.test.com">
如果需要禁止隐式的 DNS Prefetch,可以使用以下的标签:
<meta http-equiv="x-dns-prefetch-control" content="off">
借助Service Worker和cacheStorage缓存及离线开发
xperimental: 这是一个实验中的功能 此功能某些浏览器尚在开发中,请参考浏览器兼容性表格以得到在不同浏览器中适合使用的前缀。由于该功能对应的标准文档可能被重新修订,所以在未来版本的浏览器中该功能的语法和行为可能随之改变。
Cache API允许服务工作者对要缓存的资源(HTML页面、CSS、JavaScript文件、图片、JSON等)进行控制。通过Cache API,服务工作者可以缓存资源以供脱机使用,并在以后检索它们。
将项目添加到缓存
可以使用三种方法
add,addAll,set来缓存资源。add()和addAll()方法自动获取资源并对其进行缓存,而在set方法中,我们将获取数据并设置缓存。add
let cacheName = 'userSettings'; let url = '/api/get/usersettings'; caches.open(cacheName).then( cache => { cache.add.then( () => { console.log("Data cached ") }); });在上面的代码中,内部对
/api/get/usersettingsurl的请求已发送到服务器,一旦接收到数据,响应将被缓存。