性能优化的本质

优化的目的是展示更快、交互响应快、页面无卡顿情况。做优化需要理解浏览器加载和渲染的本质,可以参考 浏览器进程 认识优化渲染性能的本质

雅虎军规

包括7个类别35条军规:

  • 减少DOM节点数量:当遍历查询500和5000个DOM节点,进行事件绑定时,会有所差别。 当一个页面DOM节点过多,应该考虑使用无限滚动方案来使视窗节点可控(视频列表使用滑动窗口)。
  • 减少cookie大小:cookie 传输会造成带宽浪费,影响响应时间。消除不必要的cookies; 静态资源不需要 cookie,可以采用其他的域名,不会主动带上 cookie。
  • 避免图片src为空:图片src为空时,不同浏览器会有不同的副作用,会重新发起一起请求。

性能指标

首先,与用户感知性能相关有以下类型:

  • 页面加载时间:页面以多快的速度加载和渲染元素到页面上。
  • 加载后响应时间:页面加载和执行js代码后多久能响应用户交互。
  • 运行时响应:页面加载完成后,对用户的交互响应时间。
  • 视觉稳定性:页面元素是否会以用户不期望的方式移动,并干扰用户的交互。
  • 流畅度:过渡和动画是否以一致的帧率渲染,并从一种状态流畅地过渡到另一种状态。

Google和W3C性能工作组提供了几种性能指标有:

  • First contentful paint (FCP): 测量页面开始加载到某一块内容显示在页面上的时间。
  • Largest contentful paint (LCP): 测量页面开始加载到最大文本块内容或图片显示在页面中的时间。
  • First input delay (FID): 测量用户首次与网站进行交互(例如点击一个链接、按钮、js自定义控件)到浏览器真正进行响应的时间。
  • Time to Interactive (TTI): 测量从页面加载到可视化呈现、页面初始化脚本已经加载,并且可以可靠地快速响应用户的时间。
  • Total blocking time (TBT): 测量从FCP到TTI之间的时间,这个时间内主线程被阻塞无法响应用户输入。
  • Cumulative layout shift (CLS): 测量从页面开始加载到状态变为隐藏过程中,发生不可预期的 layout shifts 的累积分数。

其他指标:

  • DOMContentLoaded Event(DCL):DOM 解析完成时间,不包括资源加载(图片,样式表,子框架);
  • OnLoad Event(L):页面所有资源加载完成时间。

Google定义了3个最核心的指标——Core Web Vitals:

  • Largest Contentful Paint (LCP): 测量加载性能。为了能提供较好的用户体验,LCP指标建议页面首次加载要在2.5s内完成。
  • First Input Delay (FID): 测量交互性能。为了提供较好用户体验,交互时间建议在100ms或以内。
  • Cumulative Layout Shift (CLS): 测量视觉稳定性。为了提供较好用户体验,页面应该维持CLS在0.1或以内。

对应的,Google官方库 web-vitals,可以在线上或本地测量这3个指标。

LCP可能被这四个因素影响:

  • 服务端响应时间
  • Javascript和CSS引起的渲染卡顿
  • 资源加载时间
  • 客户端渲染

FID可能被这四个因素影响:

  • 减少第三方代码的影响
  • 减少Javascript的执行时间
  • 最小化主线程工作
  • 减小请求数量和请求文件大小

CLS 能被这三个因素影响:

  • 图片或视屏元素有大小属性,或者给他们保留一个空间大小,设置width、height,或者使用unsized-media feature policy
  • 不要在一个已存在的元素上面插入内容,除了相应用户输入。
  • 使用 animation 或 transition 而不是直接触发布局改变。

更多详细优化请参考 web.dev

性能工具

Google开发的这些工具都支持Core Web Vitals的测量:web-vitals 。

结合这些工具做性能优化:

  • 首先可以使用 Lighthouse,在本地进行测量,根据报告给出的一些建议进行优化;
  • 发布之后,可以使用 PageSpeed Insights 去看下线上的性能情况;
  • 接着,可以使用 Chrome User Experience Report API 去捞取线上过去30天的数据;
  • 发现数据有异常,可以使用 DevTools 工具进行具体代码定位分析;
  • 使用 Search Console's Core Web Vitals report 查看网站功能整体情况;
  • 使用 Web Vitals 扩展方便的看页面核心指标情况;

做性能优化时,会通过各种埋点,来收集用户数据,进行性能分析,简单来说是事后监控。同样的,也应该考虑事前监控,否则,每次发布需求后,去线上看数据是否下降或异常,性能优化永远作为"追赶者/弥补者"的角色在查问题、做优化。那么如何做事前监控——建立流水线机制:

  • PageSpeed Insights API 或 Lighthouse CI :把 Lighthouse 或 PageSpeed Insights API 集成到 CI 流水线中,输出报告分析。

  • Playwright Puppeteer: Puppeteer和Playwright底层都是基于 Chrome DevTools Protocol ,使用 e2e 自动化测试工具集成到流水线模拟用户操作,得到Chrome Trace Files,也就是平常录制 Performance 后,点击左上角下载的文件。。

  • Chrome Trace Files:根据谷歌提出的规则分析Trace文件,可以得到每个函数执行的时间。如果函数执行时间超过了一个临界值,可以抛出异常。如果一个函数每次的执行时间都超过了临界值,那么就值得注意了。但是还有一点需要思考的是:函数执行的时间是否超过临界值固然重要,但更重要的是这是不是用户的输入响应函数,与用户体验是否有关。

  • 输出报告。定义异常临界值。如果异常过多,考虑是否卡发布流程。

H5 页面性能极致优化实践

1 环境准备

代理使用:whistle

本地环境、测试环境模拟:nginx

数据上报:TAM,RUM

前端代码打包分析:webpack-bundle-analyzer

首先使用本地代码分析问题,然后本地模拟线上环境验证优化效果,最后再部署到测试环境进行验证。

2 性能指标选择

1. 页面加载时间

  • FCP
  • LCP
  • DCL
  • L

2. 加载后响应时间——FID;

3. 视觉稳定性——CLS

3 优化前现网性能分析

页面进度条在首屏展示后还在持续 loading,持续时间长达 10s 左右,比较影响了用户体验。而进度条的加载时长和 onload 时间密切相关,所以需要减少 onload 时长。结合实际情况,我们使用 ChromeDevTool 作为性能分析工具来观察页面性能情况。

3.1 Network 分析:观察网络资源加载耗时及顺序

通常进行网络分析需要禁用缓存、启用网络限速(4g/3g) 模拟移动端弱网情况下的加载情况,因为 wifi 网络可能会抹平性能差距。可以发现onload事件被大量媒体资源阻塞了。

网络限速(Fast 3g)条件下,DCL 时长达到 9s 多,L 时长更是到了 20s 以上:

DOM的解析受JS加载和执行的影响,找到最长请求路径文件的耗时,尽量对JS进行压缩、拆分处理(HTTP2下),能减少 DOMContentLoaded 时间。图片、视频、iFrame等资源,会阻塞 onload 事件的触发,需要优化资源的加载时机,尽快触发onload。

3.2 Performace 分析:观察页面渲染表现及JS执行情况

使用 Performance 模拟移动端注意手机处理器能力比 PC 差,因此一般将 CPU 设置为 4x slowdown 或 6x slowdown 进行模拟。

观察 Web Vitals ( FP / FCP / LCP / Layout Shift ) 核心页面指标 和 Timings 时长,发现 LCP、DCL和 Onload Event 时间较长,且出现了多次 Layout Shift(需要在 chrome console 左侧 more tools 中 Rendeing 中开启 Layout Shift Regions,在结果中的 Experience 行点击 Layout Shift ,下面的Summary 面板找到具体的偏移内容)。因此,需要使得 LCP 尽量早触发,应该减少页面大块元素的渲染时间,还有就是需要查看优化页面上存在的 Main Long Tasks 长任务数量和时长,选择在开发环境进行录制,如此可以在 Main Timeline 能看到具体的代码执行文件和消耗时长。

3.3 Lighthouse 分析:对网站进行整体评分,找出可优化项

使用 ChromeDevTool 内置 lighthouse 对页面进行跑分,发现分数比较低,TTI,SI,TBT,LCP 等不合格,CLS 达到了良好的标准。lighthouse 的评分内容是根据项目整体加载进行打分的,审查出的问题同样包含 Network、Performance 的内容,会提供一些优化建议( Oppotunities 和 Diagnostics 项),如图片大小、移除无用JS等。

4 优化加载耗时

首先,根据是否参与首屏渲染将影响 DOM 解析的 JS 资源划分为:

  • 关键 JS,采用拆分处理进行优化;
  • 非关键 JS,采用延迟异步加载进行优化。

4.1 关键JS拆分处理

使用 webpack-bundle-analyzer 进行打包分析,发现关键 JS 文件数量多,总体积大,最大文件比较大。

首先,对 Splitchunks 进行正确配置:不能简单的依靠 miniChunks 规则(比如最大公共文件 base.js 的 miniChunks = 3,会导致引用超过3次的模块就被打入该 JS)对页面依赖模块进行抽离打包,要根据具体情况拆分公共依赖。根据业务具体的需求,提取不同页面和组件都有的共同依赖(比如 utils/log/api)到 base.js中:

base: {
  name: 'base',
  priority: 50,
  minChunks: 1,
  reuseExistingChunk: true,
},

而其他未指定的公共依赖,新增一个 common.js,将阈值调高到 20 或更高(当前页面数76),让公共依赖成为大多数页面的依赖,提高依赖缓存利用率,两个文件加起来相比优化前体积减少了 50%(80kb)

其次,对公共组件进行按需加载:分析发现,对于使用 require 来加载 svg 图片,会导致 webpack 将 require 文件夹内的内容一并打包,导致页面图标组件冗余,可以通过配置 babel 的依赖加载路径调整 Icon 的引入方式,通过 import { Fire,ToTop } from 'Icons' 进行按需引入。如此之后,相比优化前体积减少 60%(54kb)。

最后,对业务组件进行代码分割(code splitting):考虑对不在首屏的页面组件进行拆分再延迟加载,减少业务代码 JS 大小和执行时长,可以使用react-loadable、@loadable/component 等库实现,也可以使用React 官方提供的React.lazy。不过,对页面组件进行代码分割会导致某些组件会有渲染的延迟,使用时应该综合用户体验和性能再做决定。

因为有使用到 TreeShaking 优化:对于没用到的包/模块/方法等,treeShaking 检查时会进行删除。可以给引入的包/模块(不是用来做 polyfill 或 shim 之类) 标记为 sideEffects: false ,只要它没有被引用到,整个模块/包都会被完整的移除。

优化前后效果对比:

总体积最大文件体积
优化前528kb215kb
优化后351kb137kb
优化效果33%36%

4.2 非关键 JS 延迟异步加载

如果在弱网情况,非关键 JS 可能会成为影响 DOM 解析的因素。

对于可能无效的缓存组件(需要特定的操作才会触发)这类的非关键 JS 资源,可以使用 Resource Hints,针对资源做 Prefetch 处理:检测浏览器是否支持 prefech,支持的情况下我们可以创建 Prefetch 链接,不支持就使用旧逻辑直接加载,这样能更大程度保证页面性能,为下一个页面提供提前加载的支持:

const getPrefetchSupported = () => {
  const link = document.createElement('link');
  const { relList } = link;
 
  if (!relList || !relList.supports) {
    return [false, link];
  }
  return [relList.supports('prefetch'), link];
};
const prefetch = (url) => {
  const [isPrefetchSupport, link] = getPrefetchSupported();
  if (isPrefetchSupport) {
    link.rel = 'prefetch';
    link.as = type;
    link.href = url;
    document.head.appendChild(link);
    } else if (type === 'script') {
      // load script
    }
};

对于其他比如监控上报等非关键JS资源,可以选择延迟加载它,或者在其他 JavaScript 之后立即加载,或者直到需要时才加载。

4.3 优化媒体资源(图片,视频)加载

对于图片,视频等媒体资源的优化参考:前端图片优化,要使其不阻塞 onload,因此需要进行懒加载处理,而且需要注意懒加载不能阻塞业务的正常展示,应该做好超时处理、重试等兜底措施

4.4 其他资源加载优化

iframe 异步加载:iframe 是会阻塞onload 的 触发的,可以将将 iframe 的时机放在 onload 之后,并使用setTimeout触发异步加载iframe,可避免iframe带来的loading影响

字体文件压缩:可能会包含很多设计指定渲染的字体,当字体文件比较大的时候,也会影响到页面的加载和渲染,可以使用 fontmin 将字体资源进行压缩生成精简版的字体文件,压缩后体积减少字体文件30%。

埋点上报优化:图片请求若耗时长就会阻塞页面 onload 事件的触发,解决方案有三种:1. 延迟合并上报;2. 使用 Beacon API ;3. 使用 post 上报。可以采用延迟合并上报的方案,更多可以参考数据上报方式梳理进行优化

5 优化页面渲染

5.1 SSR 页面优化

通过日志打点、查看 Nginx Accesslog 日志、网关监控耗时,得出以下数据:

  • SSR 服务器程序耗时是 20ms左右
  • 网关NGW -> SSR 服务器耗时 60ms 左右
  • 反向代理网关Nginx集群 -> 网关 NGW 耗时 60ms 左右

发现页面TTFB时间过长的根本原因是:NGW 网关部署和 反向代理网关 Nginx 集群、SSR 服务器程序不在同一区域,导致网络时延的产生。解决方案是让 NGW网关、反向代理网关 Nignx 集群和 SSR 服务器服务机房部署在同一区域,即执行对网关 NGW 进行扩容和分布式服务开启就近访问。

优化前后对比:

30 天网关平均耗时
优化前166 ms
优化后41 ms 优化 75%(125ms)

5.2  优化页面渲染时间

虽然,CSS文件的加载不会阻塞页面解析但会阻塞页面渲染。如果 CSS 文件较大或弱网情况,会影响到页面渲染时间,影响用户体验。通过利用 ChromeDevTool 的 More Tools 里的 Coverage 工具,录制页面渲染时 CSS 的使用率,发现首屏的 CSS 使用率只有 20%,因此考虑对页面首屏的关键CSS进行内联(利用 webpack 插件 critters 实现),与加载其余完整 CSS 文件进行分离,让首屏页面渲染不被 CSS 阻塞,这样当CSS 资源正在下载时,页面就已经能开始正常渲染显示了。

5.3 优化页面视觉稳定性(CLS)

尽量使得首屏页面内容相对固定, 页面元素出现无突兀感,避免图标缺失、背景图缺失、字体大小改变导致页面抖动或者出现非预期页面元素导致页面抖动,采用解决方案如下:

  • 确定SSR页面元素出现位置,根据数据提前做好布局
  • 首屏页面的小图可以通过base64处理,页面解析的时候就会立即展示
  • 减少动态内容对页面布局的影响,使用脱离文档流的方式或定好宽高

6 小结

优化前后Lighthouse 跑分对比(提升1倍以上)

性能得分
优化前平均 40~ 45
优化后平均 80 ~ 85

优化前后 LCP与 onload 耗时

FCP 耗时onload 耗时
优化前3.5 s6.2s
优化后1.3 s2.5s 
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐