【万字长文】前端性能优化实践 | 京东云技术团队
一、引言
从一个假死页面引发的思考: 作为前端开发,除了要攻克页面难点,也要有更深的自我目标,性能优化是自我提升中很重要的一环; 在前端开发中,会偶遇到页面假死的现象, 是因为当 js 有大量计算时,会造成 UI 阻塞,出现界面卡顿、掉帧等情况,严重时会出现页面卡死的情况;
在这里简单穿插概念之进程和线程
进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在 Windows 系统中,一个运行的 demo.exe 就是一个进程。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
进程与线程的区别
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
◦根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位;
◦资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
◦包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
◦内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的;
◦影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
◦执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行;
=> 回到阻塞原因分析
浏览器的渲染进程的线程:
浏览器有 GUI 渲染线程与 JS 引擎线程,这两个线程是互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中,等到 JS 引擎空闲时,立即被执行。js 引擎不是每次在执行更新 dom 语句时,都会停下来等 Gui 渲染引擎更新完 dom 再执行后面的 js 代码; 详见 GUI 渲染线程与 JS 引擎线程
GUI 渲染线程: 负责渲染浏览器页面,解析 HTML、CSS,构建 DOM 树、构建 CSSOM 树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。注意:GUI 渲染线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行。
JS 引擎线程: JS 引擎线程也称为 JS 内核,负责处理 Javascript 脚本程序,解析 Javascript 脚本,运行代码;JS 引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页中无论什么时候都只有一个 JS 引擎线程在运行 JS 程序;注意:GUI 渲染线程与 JS 引擎线程的互斥关系,所以如果 JS 执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程:事件触发线程属于浏览器而不是 JS 引擎,用来控制事件循环;当 JS 引擎执行代码块如 setTimeOut 时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX 异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理;注意:由于 JS 的单线程关系,所以这些待处理队列中的事件都得排队等待 JS 引擎处理(当 JS 引擎空闲时才会去执行);
定时器触发线程: 定时器触发线程即 setInterval 与 setTimeout 所在线程;浏览器定时计数器并不是由 JS 引擎计数的,因为 JS 引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待 JS 引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;注意:W3C 在 HTML 标准中规定,定时器的定时时间不能小于 4ms,如果是小于 4ms,则默认为 4ms。
异步 http 请求线程
二、案例分析
背景: 测试流水线开发过程中,为了满足用户操作方便,从节点中提取不同的指标来作为查询条件的指标来源和数据来源; 因此用户的每次操作都会进行重新计算,引起回流; 由于前端组件父子嵌套层级很多,在数据过多的时候也会形成页面假死的状态。
基于不同的性能工具检测;
1、performance Api
录制步骤: 刷新页面加载流水线详情页面,然后找到某个用例技术端,进行结果更新;因此下面的图分 3 个部分,第一个是详情页渲染; 第二段是需要大量计算的用例渲染部分; 第三段是操作结果,数据重新刷新,重新计算和渲染的部分;
其中,FPS 上方显示红色条的时候,意味着帧率特别低,用户体验特别不好。一般来说,绿色条越高,FPS 越高。 分析性能图标第一眼看 FPS;性能面板底部,图形图表的色彩越多,意味着 CPU 性能已经达到极限。当我们看到 CPU 长时间处于最大值状态,就需要考虑怎样去优化
接口的监控 + 详情页渲染 + 用例渲染;
上面的 Group 面板非常有用。我们可以很清晰明了得分析按照活动,目录,域,子域,URL 和 Frame 进行分组的前端性能;
Bottom-Up: 是 The Heavy (Bottom Up) view is available in the Bottom-Up tab,类似事件冒泡;
Call Tree:是 And the Tree (Top Down) view is available in the Call Tree tab,类似事件捕获;
更新结果,页面重新绘制,计算;
黄色(Scripting):JavaScript 执行
紫色(Rendering):样式计算和布局,即重排
蓝色(Loading):网络通信和 HTML 解析
绿色(Painting):重绘
灰色(System):其它事件花费的时间
白色(Idle):空闲时间: 可能是同时请求了很多接口,promise.all 需要等待所有接口返回成功后才会渲染页面,idle 时间变长了很多,浏览器一直在等待接口全部返回;
用例列表和添加按钮部分产生了偏移, 红色 Layout Shift;
2、lightHouse 分析数据;分数很低;
3、performace monitor: 加载过程中,CPU 飙 80%;
主要可改善指标如下:
TBT: total Blocking Time 总阻塞时间
CLS:记录了页面上非预期的位移波动
相关名词解析:https://web.dev/metrics/;
三、性能优化三大核心指标:LCP FID CLS
上述的指标太多了,哪些才是核心指标呢? Google 在 2020 年五月提出了网站用户体验的三大核心指标
1、Largest Contentful Paint (LCP)
LCP:最大内容绘制 , 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但 LCP 能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
最大元素:
◦<img> 标签
◦<image> 在 svg 中的 image 标签
◦<video> video 标签
◦CSS background url()加载的图片
◦包含内联或文本的块级元素
影响元素:
◦服务端响应时间----接口性能
◦Javascript 和 CSS 引起的渲染卡顿 ✨✨✨---webWork.js 计算问题
◦资源加载时间✨✨✨---CDN
◦客户端渲染✨✨---SSR
2、First Input Delay (FID):
FID:首次输入延迟, 代表了页面的交互体验指标,就是看用户交互事件触发到页面响应中间耗时多少,如果其中有长任务发生的话那么势必会造成响应时间变长,推荐响应用户交互在 100ms 以内。
影响因素
◦减少第三方代码的影响
◦减少 Javascript 的执行时间
◦最小化主线程工作
◦减小请求数量和请求文件大小
3、Cumulative Layout Shift (CLS)
CLS 代表了页面的稳定指标,它能衡量页面是否排版稳定。尤其在手机上这个指标更为重要,因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做的很差。CLS 的分数在 0.1 或以下,则为 Good。
影响因素: 通过下面的原则避免非预期布局移动:
◦图片或视屏元素有大小属性,或者给他们保留一个空间大小,设置 width、height,或者使用 unsized-media feature policy 。
◦不要在一个已存在的元素上面插入内容,除了相应用户输入。
◦使用 animation 或 transition 而不是直接触发布局改变。
四、如何使用性能检测工具?
Lighthouse(速度也比较快, 支持 json 指标产出和页面 html 产出),在本地进行测量,根据报告给出的一些建议进行优化;---例如 CLS,用这个就很方便, 满足分值后或者通过评审后上线;
项目发布上线后,我们可以使用 PageSpeed Insights 去看下线上的性能情况;
使用 PageSpeed Insights,可以使用 Chrome User Experience Report API 去捞取线上过去 28 天的数据;
数据有异常,可使用 DevTools 工具进行具体代码定位分析;---performance 分析, 方案总结和优化
使用 Search Console’s Core Web Vitals report 查看网站功能整体情况;
使用 Web Vitals 扩展方便的看页面核心指标情况;
五、页面过程详细解析
上述讲了网络请求,渲染过程,那下面我们看下一个简单的页面的整个过程是怎么样的;
导航阶段,该阶段主要是从网络进程接收 HTML 响应头和 HTML 响应体。
解析 HTML 数据阶段,该阶段主要是将接收到的 HTML 数据转换为 DOM 和 CSSOM。
生成可显示的位图阶段,该阶段主要是利用 DOM 和 CSSOM,经过计算布局、生成层树 (LayerTree)、生成绘制列表 (Paint)、完成合成等操作,生成最终的图片。
1. 导航阶段,请求 HTML 数据阶段:
◦该任务的第一个子过程就是 Send request,该过程表示网络请求已被发送。然后该任务进入了等待状态。
◦接着由网络进程负责下载资源,当接收到响应头的时候,该任务便执行 Receive Respone 过程,该过程表示接收到 HTTP 的响应头了。
◦接着执行 DOM 事件:pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。
◦些事件被处理完成之后,那么接下来就接收 HTML 数据了,这体现在了 Recive Data 过程,Recive Data 过程表示请求的数据已被接收,如果 HTML 数据过多,会存在多个 Receive Data 过程。
◦等到所有的数据都接收完成之后,渲染进程会触发另外一个任务,该任务主要执行 Finish load 过程,该过程表示网络请求已经完成。
2. 解析 HTML 数据阶段
其中一个主要的过程是 HTMLParser:解析的上个阶段接收到的 HTML 数据。
◦在 ParserHTML 的过程中,如果解析到了 script 标签,那么便进入了脚本执行过程,也就是图中的 Evalute Script。
◦DOM 生成完成之后,会触发相关的 DOM 事件,比如:典型的 DOMContentLoaded,还有 readyStateChanged。
◦DOM 生成之后,ParserHTML 过程继续计算样式表,也就是 Reculate Style,这就是生成 CSSOM 的过程
3. 生成可显示位图阶段
该阶段需要经历布局 (Layout)、分层、绘制、合成等一系列操作:
在生成完了 DOM 和 CSSOM 之后,渲染主线程首先执行了一些 DOM 事件,诸如 readyStateChange、load、pageshow。
总而言之,大致过程如下:
◦首先执行布局,这个过程对应图中的 Layout。
◦然后更新层树 (LayerTree),这个过程对应图中的 Update LayerTree。
◦有了层树之后,就需要为层树中的每一层准备绘制列表了,这个过程就称为 Paint。
◦准备每层的绘制列表之后,就需要利用绘制列表来生成相应图层的位图了,这个过程对应图中的 Composite Layers。走到了这步,主线程的任务就完成了。
接下来主线程会将合成的任务完全教给合成线程来执行,下面是具体的过程,也可以对照着 Composite、Raster 和 GPU 这三个指标来分析
首先主线程执行到 Composite Layers 过程之后,便会将绘制列表等信息提交给合成线程,合成线程的执行记录你可以通过 Compositor 指标来查看。合成线程维护了一个 Raster 线程池,线程池中的每个线程称为 Rasterize,用来执行光栅化操作,对应的任务就是 Rasterize Paint。当然光栅化操作并不是在 Rasterize 线程中直接执行的,而是在 GPU 进程中执行的,因此 Rasterize 线程需要和 GPU 线程保持通信。然后 GPU 生成图像,最终这些图层会被提交给浏览器进程,浏览器进程将其合成并最终显示在页面上。
六、性能优化实践方案
对于前端应用来说,网络耗时、页面加载耗时、脚本执行耗时、渲染耗时等耗时情况会影响用户的等待时长,
而 CPU 占用、内存占用、本地缓存占用等则可能会导致页面卡顿甚至卡死。
因此,性能优化可以分别从耗时和资源占用两方面来解决,也可以理解成时间和空间两个方面。
时间角度(耗时)
在时间角度进行优化主要是减少耗时,浏览器在页面加载的过程中,主要会进行以下的步骤:
网络请求相关(发起 HTTP 请求从服务端获取页面资源,包括 HTML/CSS/JS/图片资源等)浏览器解析 HTML 和渲染页面加载 Javascript 代码时会暂停页面渲染(包括解析到外部资源,会发起 HTTP 请求获取并加载)
耗时优化的着手点:
1、网络请求优化: 目标在于减少网络资源的请求和加载耗时,如果考虑 HTTP 请求过程,显然我们可以从几个角度来进行优化:
请求链路:DNS 查询、部署 CDN 节点、缓存等
对于请求链路,核心的方案常常包括使用缓存,比如 DNS 缓存、CDN 缓存、HTTP 缓存、后台缓存等等,前端的话还可以考虑使用 Service Worker、PWA 等技术。使用缓存并非万能药,很多使用由于缓存的存在,我们在功能更新修复的时候还需要考虑缓存的情况。除此之外,还可以考虑使用 HTTP/2、HTTP/3 等提升资源请求速度,以及对多个请求进行合并,减少通信次数;对请求进行域名拆分,提升并发请求数量。
数据大小:代码大小、图片资源等
数据大小则主要考对请求资源进行合理的拆分(CSS、Javascript 脚本、图片/音频/视频等)和压缩,减少请求资源的体积,比如使用 Tree-shaking、代码分割、移除用不上的依赖项等。在请求资源返回后,浏览器会进行解析和加载,这个过程会影响页面的可见时间,通过对首屏加载的优化,可有效地提升用户体验。
2、首屏加载优化: 首屏加载优化核心点在于两部分:
◦将页面内容尽快地展示给用户,减少页面白屏时间。
◦将用户可操作的时间尽量提前,避免用户无法操作的卡顿体验。
我们的页面也需要在客户端进行展示,此时可充分利用客户端的优势:
◦配合客户端进行资源预请求和预加载,比如使用预热 Web 容器配合客户端将资源和数据进行离线,可用于下一次页面的快速渲染使用秒看技术,通过生成预览图片的方式提前将页面内容提供给用户除了首屏渲染以外,用户在浏览器页面过程中,也会触发页面的二次运算和渲染,此时需要进行渲染过程的优化
3、渲染过程优化:渲染过程的优化可以理解成首屏加载完成后,用户的操作交互触发的二次渲染。主要思路是减少用户的操作等待时间,以及通过将页面渲染帧率保持在 60FPS 左右,提升页面交互和渲染的流畅度。包括但不限于以下方案:
◦使用资源预加载,提升空闲时间的资源利用率--preload
◦减少/合并 DOM 操作,减少浏览器渲染过程中的计算耗时
◦使用离屏渲染,在页面不可见的地方提前进行渲染(比如 Canvas 离屏渲染)
◦通过合理使用浏览器 GPU 能力,提升浏览器渲染效率(比如使用 css transform 代替 Canvas 缩放绘制)
以上这些,是对常见的 Web 页面渲染优化方案。对于运算逻辑复杂、计算量较大的业务逻辑,我们还需要进行计算/逻辑运行的提速。
什么是重绘和回流
回流比重绘更加消耗性能,付出的代价更高。
◦recalculate style (style):结合 DOM 和 CSSOM,确定各元素应用的 CSS 规则
◦layout:重新计算各元素位置来布局页面,也称 reflow
◦update layer tree (layer):更新渲染树
◦paint:绘制各个图层
◦composite layers (composite):把各个图层合成为完整页面
核心: 布局会不会变! 回流一定会导致重绘,重绘不一定导致回流
1.重绘:简单来说就是重新绘画,当给一个元素更换颜色、更换背景,虽然不会影响页面布局,但是颜色或背景变了,就会重新渲染页面,这就是重绘。
2.回流: 当增加或删除 dom 节点,或者给元素修改宽高时,会改变页面布局,那么就会重新构造 dom 树然后再次进行渲染,这就是回流。
哪些会引起回流呢?
改变 dom 元素的几何属性,常见的几何属性有 width、height、padding、margin、left、top、border 等等。
改变 dom 树的结构,主要指的是增加或减少 dom 节点,移动等操作。
获取一定特殊的属性值,如属性:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 时,你就要注意了!除此之外:调用了 getComputedStyle 方法,也会触发回流。
总结
重绘不会引起 dom 结构和页面布局的变化,只是样式的变化,有重绘不一定有回流。
回流则是会引起 dom 结构和页面布局的变化,有回流就一定有重绘。
怎么进行优化或减少?
多个属性尽量使用简写,例如:boder 可以代替 boder-width、boder-color、boder-style
创建多个 dom 节点时,使用 documentfragment 创建
避免使用 table 布局
避免设置多层内联样式,避免节点层级过多!!!
避免使用 css 表达式
将频繁重绘或回流的节点设置为图层,图层能够阻止该节点的渲染行为影响到别的节点(例:will-change\video\iframe 等标签),浏览器会自动将该节点变为图层
1.计算/逻辑运行提速
计算/逻辑运行速度优化的主要思路是“拆大为小、多路并行”,方式包括但不限于:
通过将 Javscript 大任务进行拆解,结合异步任务的管理,避免出现长时间计算导致页面卡顿的情况
将耗时长且非关键逻辑的计算拆离,比如使用 Web Worker
通过使用运行效率更高的方式,减少计算耗时,比如使用 Webassembly
通过将计算过程提前,减少计算等待时长,比如使用 AOT 技术
通过使用更优的算法或是存储结构,提升计算效率,比如 VSCode 使用红黑树优化文本缓冲区的计算
通过将计算结果缓存的方式,减少运算次数
空间角度(资源)
在做性能优化的时候,其实很多情况下会依赖时间换空间、空间换时间等方式,只能根据自己项目的实际情况做出取舍,选择相对合适的一种方案去进行优化。
资源占用常见的优化方式包括:
1.合理使用缓存,不滥用用户的缓存资源(比如浏览器缓存、IndexDB),及时进行缓存清理;
2.避免存在内存泄露,比如尽量避免全局变量的使用、及时解除引用等
3.避免复杂/异常的递归调用,导致调用栈的溢出
4.通过使用数据结构享元的方式,减少对象的创建,从而减少内存占用
七、性能提升解决方案实践:
实践 1: 单线程到多线程的实践
注意:new Worker(xxx.js)里的 xxx.js 必须和 HTML 文件同源必须在 http/https 协议下访问 HTML 文件,不能用文件协议(类似 file:///E:/wamp64/www/t.html 这种
因此我用 mamp 搭建的一个环境指向测试的文件夹;本地配置http://www.performance.com/进行测试;
worker.js
在 vue.js 中的使用过程, 除了配置,其他同上;
更新后,当前页面代码段中,计算 300ms+ 提升,效果一般,主要还是 dom 节点过多引起的;
Web Worker 的限制
1.在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象
2.Worker 中只能获取到部分浏览器提供的 API,如定时器、navigator、location、XMLHttpRequest 等
3.由于可以获取 XMLHttpRequest 对象,可以在 Worker 线程中执行 ajax 请求
4.每个线程运行在完全独立的环境中,需要通过 postMessage、 message 事件机制来实现的线程之间的通信
计算的运算时长 - 通信时长 > 50ms,推荐使用 Web Worker
实践 2, webpack 配置方向
1.生产环境关闭 productionSourceMap、css sourceMap
SourceMap 就是当页面出现某些错误,能够定位到具体的某一行代码,SourceMap 就是帮你建立这个映射关系的,方便代码调试;
关闭 css sourceMap 以后,js 文件从 55M 降低到 12M, 文件从 650 个减少至 325 个;
实践 3、网络请求优化-CDN 资源引入
分析大文件, 目前项目的情况: js 总计 12.1M (325 个项目); css 总计 11.1M (249 个项目),
◦element-ui 年 1.85M--涉及改造的有点多,暂时不更新;
◦Ecahrts 2.55M --cdn 引入
◦handsontable 3.34M---在线引入后,由于 handsontable-vue 还会安装 handsontable, 因此需要把 handsontable-vue 也使用在线的方式,cdh 资源可在https://www.jsdelivr.com/package/npm/@handsontable/vue中找到;
◦vue-json-editor 1.24M--没找到在线 js 资源,建议组件引入的时候,直接使用 json-editor;
◦将资源进行 cdn 方式引入;使用 CDN 引入以后,js 总计 9.3M(326 个项目), css 总计 10.9(249 个项目)
安装 webpack-bundle-analyzer
vue.config.js
实践 4、通过 compression-webpack-plugin 插件把代码压缩为 gzip。但是!需要服务器支持 webpack 端 vue.config.js 配置如下:
使用 CDN 引入以后,js 总计 9.3M(326 个项目)=> 3.1M, css 总计 10.9(249 个项目)--没有发生变化
nginx 的配置, 这篇文章很不错https://www.cnblogs.com/wwjj4811/p/15847916.html, 这块我没线上测试;不过应该没啥问题;
实践 5、tree-shaking
tree shaking:通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export,是 webpack 4 版本,扩展的这个检测能力;
使用 tree-shaking 以后,js 总计 3.1M(326 个项目)=> 2.9M(315 个项目), css 总计 10.9(249 个项目)--384K(15 个项目)
作者:京东零售 苏文静
来源:京东云开发者社区 转载请注明来源
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/131639a16bee65cbea7afa7f6】。文章转载请联系作者。
评论