深圳幻海软件技术有限公司 欢迎您!

京东PLUS前端H5性能优化实践

2023-02-28

​前言随着移动互联网的发展,用户对产品的使用体验要求越来越高。H5作为业务的重要载体,应用非常广泛,如何把控好H5的性能是一门重要的工作。因此H5页面性能是一个非常核心的用户体验指标。研究表明一个网页加载的快慢会直接影响用户在这个页面逗留的时长,用户在发现页面有内容呈现的时候,如果在页面进行了点击但

​前言

随着移动互联网的发展,用户对产品的使用体验要求越来越高。H5 作为业务的重要载体,应用非常广泛,如何把控好 H5 的性能是一门重要的工作。因此 H5 页面性能是一个非常核心的用户体验指标。

研究表明一个网页加载的快慢会直接影响用户在这个页面逗留的时长,用户在发现页面有内容呈现的时候,如果在页面进行了点击但是页面没有立即响应,用户通常会认为页面加载出现了延迟;如果页面在加载过程中某些元素发生了大幅度的偏移,这对于用户来说体验是相当糟糕的。

因此提高用户体验应从页面的渲染速度、响应能力及页面的抖动情况考虑,目前比较主流的衡量页面指标的参数是谷歌 lighthouse 提出的几个指标, 主要包括 FCP(First Contentful Paint), LCP(Largest Contentful Paint), CLS(Cumulative Layout Shift), TTI(Time To Interactive), TBT(Total Blocking Time)等。下面我们将从FCP、LCP、CLS、TTI、TBT五个指标来进行一一介绍。

FCP

FCP(First Contentful Paint)是指页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间,也就是我们通常所说的首次内容绘制时间,它是测量感知加载速度的一个重要指标。那么什么“内容”才算FCP的呢,其中包含了文本、图像(包括背景图像)、<svg>元素或非白色的<canvas>元素。

LCP

LCP(Largest Contentful Paint)是根据页面首次开始加载的时间点来报告可视区域内最大图像或文本块完成渲染的相对时间。最大内容绘制表示着页面的主要内容已经加载完成,它的检测是通过Element Timming API来提供支持,使用的也是通过性能监听事件的一个方式。那哪些元素在LCP的计算之内呢?除了常见的<img>元素,<video>元素、通过url元素加载的背景图像以及包含文本节点或其他行内级文本元素子元素的块级元素都在计算范围之内。需要注意的是LCP的计算是一个动态的过程。

CLS

CLS(Cumulative Layout Shift)是指整个页面中所有意外布局偏移中最大一连串的布局偏移分数,即我们通常所说的累计布局偏移,其中一连串的布局偏移,也叫会话窗口,是指一个或多个连续发生的单次布局偏移,每次偏移相隔的时间少于1秒,且整个窗口的最大持续时长为5秒。就像下图中的灰色文字区域在页面加载时如果发生了位移,那么它的位移就会被计入CLS的分数。

TTI

TTI(Time To Interactive)是指页面从开始加载到主要子资源完成渲染,并且能够快速、可靠的响应用户输入的时间,通常我们也叫可交互时间。那么TTI是如何计算的呢,如下图首先沿时间轴正向搜索时长至少为5秒的安静窗口(安静窗口是指没有长任务且不超过两个正在处理的网络get请求),然后沿时间轴反向搜索安静窗口之前的最后一个长任务,如果没有找到长任务,则在FCP步骤停止执行,TTI就是安静窗口之前最后一个长任务的结束时间,如果没有找到长任务的话,则与FCP值相同。

TBT

TBT(Total Blocking Time)是测量FCP与TTI之间的总时间,这期间,主线程被阻塞的时间过长,无法作出输入响应。我们通常把任务时间超过50毫秒的任务称之为长任务,那么一个页面的总阻塞时间其实就是FCP和TTI之间发生每个长任务的阻塞时间总和。如下图淡红色区域的时间总和就是这个页面的TBT分数。

如何检测页面性能?

在介绍了相关的指标之后,如何知道一个页面性能的好坏呢?我们PLUS团队联合烛龙团队在基于lighthouse的基础上,结合我们京东实际的业务情况,共同开发了一套性能监控的SDK,在其中明确的提出了H5和PC两套性能评分标准,并且给出了性能良好与较差的阈值。

如何做性能优化?

在深入了解各项指标之后,我们来看一下如何对页面进行性能优化。一个页面从输入URL到页面最后在浏览器的渲染流程大致如下:

  • URL解析 => DNS解析 => 建立TCP连接 => 客户端发送请求 => 服务器处理和响应请求 => 浏览器解析并渲染响应内容 => 断开TCP链接

可以看到其中从URL解析开始到服务器响应请求的过程都是属于网络层面,浏览器解析渲染相应内容属于渲染层面,接下来我们就将从网络层面和渲染层面这两个大方向来看一下如何去做性能优化。

我们都知道,在网络层面其实对于前端工程师来讲可优化的点并不多,但是通过网络层面的优化,可以极大程度上降低TTFB的等待时间,从而提高FCP、LCP、TTI、TBT这些指标的分数,下面是一些常见优化方式:

  • 缓存技术:我们可以合理的使用缓存,如CDN缓存、浏览器缓存以及应用离线包,来减少资源的请求。除了这些,还可以在接口中设置Expires 和 Cache-Control 利用http缓存来减少接口的请求时间。
  • 服务端还可以使用http2/http3、减少重定向等方式来保证我们的接口响应速度。

在了解了网络层面的一些优化方式之后,我们主要来看下在渲染层面如何进行优化,首先,我们来看一下浏览器的渲染机制,大概分为以下几个步骤:

  • 解析HTML,生成DOM树,解析CSS,生成CSSOM树。
  • 将DOM树和CSSOM树结合,生成渲染树。
  • 计算图层布局。
  • 绘制图层。
  • 合成图层,显示页面。

在解析HTML的过程中会去请求页面所需的js/css等静态资源,对于资源请求这部分,我们可以进行一些特定的优化,让我们的资源能够更快速的进行加载,减少TTFB等待时间,从而提高FCP、LCP、TTI以及TBT的分数,比如:

  • 可以通过添加preconnect和dns-prefetch属性预链接所需要的源,其中preconnect可以提前建立连接,但是如果在10秒内没有使用连接,浏览器会关闭它,从而浪费一些早期的连接占用CPU的时间,dns-prefetch会在请求资源时提前进行域名解析。
  • 如果我们想要提前加载资源,可以添加preload属性来预先加载一些关键的资源,帮助页面更快的进行渲染。
  • 还可以将不重要的script脚本添加async和defer属性,然后提取重要的css,多余的css分割成不同的文件,将不重要的文件添加media属性来消除阻塞渲染。

如果想更快的完成资源的请求,我们也可以对资源进行优化,使产出的文件体积更小,文件请求耗时更短,这也能使我们的FCP和LCP分数得以提高:

  • 移除未使用的css/js,如何能够快速检查出哪些代码我们有用到呢,可以打开Chrome => 开发者工具 => shift + alt + p 搜索Coverage Tab,点击刷新可以看到资源的请求状况以及可用代码覆盖率,然后点击打开每个资源,可以查看具体内容有没有被使用,其中绿色部分为关键代码,红色部分为非关键代码,可以通过删除红色部分代码来减小静态资源的体积。

  • 通过webpack/rollup/gulp等构建工具对代码进行打包压缩
  • 对资源进行gzip压缩 
  • 将资源进行拆分(包括css、js),并按需引用,并且延迟加载优先级较低的js
  • 对于图片我们可以使用工具进行图像压缩,也可以使用图像cdn,格式最好使用webp,将大大减少图片的大小,对于大型的gif可以转为视频

在解析完html之后,浏览器会识别并加载所有的 CSS 样式信息与 DOM 树合并,最终生成页面渲染树,我们在写css的时候,需要注意以下几点,这样可以使我们的页面能够更快速的渲染出来。

  • 减少css选择器的层级,避免多层嵌套
  • 避免使用较多的css表达式
  • 对于css动画,尽量使用 transform(transform可开启硬件加速)
  • 对于一些特定的动画,可以选择使用requestAnimationFrame 代替

当浏览器生成渲染树以后,就会根据渲染树来进行布局,然后图层会进行计算和绘制,最后合成图层在页面显示出来。在这个渲染的过程中,我们可以进行以下的优化:

  • 避免DOM过大或者多层嵌套
  • 避免批量的对DOM进行修改

除了对网络和渲染两个层面优化之外,我们也可以针对特定的指标进行优化。

1. 对于FCP和LCP,还可以在HTML中增加骨架屏,或者使用服务端渲染的方式进行优化,同时也可以提升用户体验。

2. 对于CLS该如何进行优化呢?我们首先来分析一下CLS偏移较大产生的原因,常见的原因主要有:无尺寸的头像、广告,动态注入的内容,在更新 DOM 之前等待网络响应的操作等。那么如何解决呢?我们可以给图像设置长宽比,增加占位符,给动态注入的元素留出预留空间,有些情况下虽然不能完全避免位移偏差,但会尽可能减少CLS。

3. 对于TTI和TBT,影响这两个指标的其实最多的就是长任务的执行,长任务可以在浏览器performance面板查看(如下图),我们点击图中红色长任务可以看到这个任务中执行的内容,从而去分析做一些优化,这里也提供几点对与长任务优化的建议:

  • 页面首屏的数据最好聚合到一个接口中,这样既能保证在接口返回之后首屏可以直接渲染,还能避免多个接口请求引起的长任务。
  • 减少页面的重复渲染,如使用react hooks中的 useMemo、useCallback,加入钩子依赖来避免组件的重复渲染。
  • 降低不重要接口请求的优先级,如埋点上报,可以利用requestIdleCallback api在浏览器空闲时再进行上报。

PLUS性能优化实践

我们在分析了如何对页面进行性能优化之后,来看一下PLUS团队在性能优化方面做的一些实践。我们选用了PLUS流量比较大的两个页面作为实验,由于这两个页面在线上已经迭代了很多年并且页面中之前老逻辑设计的比较复杂,所以导致这两个页面各项性能指标相对较低。未试用首页分数、会员店分数都较低。我们在分析了相关的代码之后,对各项指标做了专门的优化。

拿未试用首页来讲:

1. 在FCP和LCP方面,添加了DNS预热并且对骨架屏做了调整。

  • 首先给页面中的静态资源、图片地址设置了DNS预解析。通过 dns-prefetch 提前建立域名解析。当浏览网页时,浏览器会在加载过程中对网页中的域名进行解析缓存,这样在点击当前网页连接时无需进行DNS解析,从而减少等待时间。

  • 还对骨架屏也进行了调整,通过在HTML里设置骨架屏,在页面数据尚未加载前,先给用户展示出大致结构,直到请求返回后再补充需要显示的数据内容,既降低了用户的焦灼情绪,又能使页面加载过程变得自然通畅,不会造成长时间的白屏或闪烁,同时也提高了FCP的分数。

2. CLS方面,我们通过分析得知,影响CLS的主要原因是如果在APP内使用沉浸式头的话,由于沉浸式头部是异步调用获取的,就导致它会造成头部高度偏移,页面抖动。我们通过判断不同机型给定不同的沉浸式初始高度,等待沉浸式异步返回后再重新设置,从而减少了页面抖动及布局偏移。优化前后CLS对比如下图:

3. 在TTI和TBT方面,我们做了如下优化:

  • 首先对接口优先级做了调整,针对业务本身,评估接口调用优先级。优先请求首屏核心接口,针对可能出现的造成网页偏移的数据字段优先处理,统一返回,从而减少位移。
  • 在打包优化上,通过webpack对业务包和公用包进行抽离,从而降低单个包体积过大的问题。
  • 利用浏览器空闲时间进行埋点上报和低优先级接口请求,在页面中有许多的权益Icon图标的埋点,我们把这些埋点上报的请求利用浏览器requestIdleCallback Api使请求在浏览器空闲时再进行上报,从而减少主线程的繁忙程度,更快的完成首屏渲染,以下是封装的requestIdleCallback的部分代码:

  • 楼层懒渲染:楼层内容不在首屏展示,因此初始化楼层填充 loading 进行占位,通过 observe 监听楼层出现在可视区再进行真实接口请求及渲染。这样的话大大减少了首屏要请求的接口数量。

经过以上的优化,我们可以看一下未试用首页的最终成果。从视频的加载效果来看明显是比之前好的。PLUS会员店优化后性能也获得了大幅度提升。

未试用首页-优化前后对比视频

QQvmeQ9T" id="h26976cb-QQvmeQ9T">结语

在前端的领域中,性能优化是个永久的话题,性能优化的经验既需要对技术极致的追求,也需要持续的积累沉淀。未来我们将做更深入的探索,如使用webWorker,Hybrid等技术持续优化前端H5页面,以给到用户更好的体验。