技术分享
基于PageSpeed评分性能优化实践
00 分钟
2023-1-19
2023-9-11
type
status
date
slug
summary
tags
category
icon
password

一、性能指标

针对线上项目做性能优化,首先需要有确定可量化的评判标准,用来判断优化工作是否有效。

发展

传统的性能指标典型的是 DOM Ready时间和页面加载时间(load time),在早期前后端耦合的时代,通过服务端使用模板渲染出HTML,能比较反映出网站性能,后面前端领域的迅猛发展,尤其在客户端渲染方案的盛行,以及各种动态技术的大量运用,所以这两个指标已经失去原有的意义,无法准确反映性能:
 
DOM Ready:初始化HTML文档被完全加载和解析完成时间,一般通过监听 DOMContentLoaded 事件来获取。
页面加载时间: 整个页面所需资源(脚本、样式、图片等)加载完成的时间,通过监听全局的 load 事件来获取。

演进

现在浏览器提供了 Navigation Timing API,通过perperformance.timing 可以获取从页面加载到结束整个过程中不同阶段的时间点,开发者可以通过从多个维度去定义关键指标,比如基于timming API主要定义了以下七个关键指标:
 
  • DNS (domainLookupEnd - domainLookupStart)
  • Connect (connectEnd -connectStart)
  • Request (responseStart - requestStart)
  • Response (responseEnd - responseStart)
  • Blank(domInteractive - responseStart)
  • Domready (domContentLoadedEventEnd - navigationStart)
  • Onload (loadEventEnd - navigationStart)
 
这些指标更侧重于技术细节,并不能很好地反映用户真正关心的问题,在做性能优化时,很可能面临已经把某些特定指标如加载时间的数值大幅减少,但是用户体验仍然很差,基于此问题,Chrome团队和W3C性能工作组推出一组以用户为中心的性能指标,从用户角度更好地评判页面性能。这些指标包含了:
 
  • SI(Speed Index)速度指数
    • 速度指数衡量页面加载期间内容的视觉显示速度。Lighthouse 首先在浏览器中捕获页面加载的视频,并计算帧之间的视觉进度。Lighthouse 然后使用Speedline Node.js 模块生成速度指数分数。
  • FP(First Paint)首次绘制
  • FCP (First Contentful Paint) 首次内容绘制
    • 是指页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。“内容”可以是文本、图像(包括背景图)、<svg>元素或非白色的<canvas>元素。这个指标回答了用户的问题,页面正在运行中。除此还有一个从名称上很接近的指标,FP(首次绘制),两者之间的区别:
      FP 可以认为是白屏时间
      FCP 可以认为是首屏时间
       
  • LCP (Largest Contentful Paint)最大内容绘制
    • 是指在可视区域内的最大图片或文本块完成渲染的时间,元素大小指的是内容占据的面积大小,即 size = width * height ,不包含边距边框。这个指标回答了用户问题,页面是否呈现了对用户有用的内容,早先也有类似的指标FMP(首次有效绘制),但是由于绘制的定义通常很难解释,且算法经常出错,所以LCP表达更加简单明了。
       
  • TTI (Time to Interactive)可交互时间
    • 在 Lighthouse 中,可交互时间指标有了更通用、标准化的定义,TTI 应从 FCP 时间点开始沿时间轴查找,如果出现 5 秒的静默窗口(没有长任务并且不超过 2 个正着处理的 GET 请求),那么最后一个长任务介绍的时间点即为可交互时间。
      长任务指的是执行时间超过 50 ms 的任务。
      主线程上若是存在导致阻塞状态的长任务,将导致无法响应用户交互。
      notion image
 
  • TBT (Total Blocking Time)总阻塞时间
    • TBT 是指在 FCP 和 TTI 之间所有长任务超过 50ms 的部分的时间总和(注意不是长任务的时间总和),TBT 和 TTI 是一对配套指标,用于衡量在页面可交互之前的阻塞程度。
       
  • CLS(Cumulative Layout Shift)累计布局偏移
    • 用于衡量页面视觉稳定性,单次布局偏移分数是影响分数(不稳定区域占可是区域的百分比)与距离分数(不稳定元素最大位移距离占比)的乘积。
      CLS 指标本身一直在不断进化 ,便于更加准确地去衡量布局偏移对用户的影响。
 

二、性能测量

Ligthuse

Google 的 Lighthouse ,最初作为一个独立的浏览器扩展程序需要开发者自行安装(支持 Firefox),目前已经集成到 Chrome DevTools 。Lighthouse 不仅仅是一个性能测量工具,除此之外还提供 PWA 、SEO 、可访问性、最佳实践等审计报告。
 
在做性能优化的时候,如何有效评估优化方案的效果是一个问题,由于还没有发布到线上环境无法采集真实用户性能数据,这时候使用工具进行实验室测量就显得至关重要。
同时,Lighthouse 提供开源 CI 工具 Lighthouse CI 开发者能自行部署服务,并集成到现有的 CI 体系中。
 

三、性能优化方案

通过Ligthuse生成的报告,我们可以确定优化的方向,接下来是如何实施具体的优化方案,性能优化是老生常谈,同时与时俱进的主题,早期大名鼎鼎的雅虎35条性能军规到现在大部分仍然适用,其中必须结合现有的技术栈来针对性地实施方案。

3.1、减少包体积

包体积问题,主要表现在: 冗余代码、复制粘贴的重复代码、非必须要大体积的类库、未经优化图片文件。
  • 冗余代码
    • 产生冗余代码的方式有多种,比如导入被废弃的功能模块、在A/B测试完成后未完全移除版本代码、可以借助Webpack插件 webpack-bundle-analyzer 以一种可视化的方式呈现每个包的具体模块信息、大小、包含工具分析出运行过程中文件(脚本和样式)的使用情况,可以作为参考更好地针对性瘦身优化。
  • 重复代码
    • 重复性代码往往是实现相似功能的偷懒行为,直接从复制粘贴旧代码进行修改导致,可以借助 JSinspect检测相同或相似代码,然后进行合理抽象。还有一种情况,依赖NPM包提供多种方式的代码,比如dist目录下的CommonJS、es目录下的ES Modules代码,如果在不同地方引入不同模块标准的包,就等于引入重复功能模块。更甚一步,在跨团队合作中依赖包只提供打包版本,也会出现babel polyfill代码多次重复,且无从分析,一般解决方案是制定统一的标准,推荐 NPM 包都提供仅 babel 编译不打包版本。
  • 类库开销
    • 在类库的使用上同样需要注意,比如仅使用一两个方法就引入整个 lodash 库,推荐做法是按需引入,不用改变写法加入 babel-plugin-lodash 这类插件就能在代码构建时转换。另外一种情况是引入 moment 这类体积较大的库用作时间处理与格式化,可以视实际情况采用体积更小的替代品。对于更简单的需求,则完全可以基于原生 API 自行实现封装一些方法。
  • 图片文件
    • 未经优化的图片可高达几百 KB ,应在保证图片清晰度的情况压缩大小。另一方面,为现代浏览器提供有更高效压缩算法的图片格式,相比传统的 PNG 和 JPG 格式,WebP 在同等质量下有更小的体积,注意做好降级方案。同时在webapck编译时增加10kb及以下图片转换成base64处理,减少页面图片请求个数。

3.2、优化资源加载

作为开发者做好包体积优化能节省网络传输时间,以及一部分代码执行时间,但更重要的是让资源有效加载,可从资源加载顺序和优先级方面着手。
  • Service Worker
    • 使用 Service worker 缓存预载资源,对后续访问会有极大的性能提升,能节省大量网路传输开销。在项目中推荐采用 Google 提供的 Workbox 库,可以通过配置的方式对不同类型资源应用不同缓存策略。Service Worker 带来的优化效果不能从 PageSpeed Insights 网站上的分数直接体现,因为 PageSpeed 总是单次分数并且不使用缓存。
  • 优化加载第三方脚本
    • 应用依赖的第三方脚本通常会减慢页面加载速度,一般采用以下方式:按需加载和延迟加载。
    • 按需加载
      • 需用户交互才用到的功能模块应按需加载。举个例子,用户登录时要调用一个第三方验证模块,就没必要在页面一开始就引入该脚本,在用户执行登录操作时引入更合理。
    • 延迟加载
      • 像是 Google analysis 和合作商营销等第三方日志埋点脚本,业务需要无法移除,加载后占用大量性能资源。由于本身没有依赖关系,可使用 defer script 延迟脚本的解析执行。更进一步,延迟到在可交互时间之后加载就基本不会有任何影响。
  • Resource Hints
    • 为了使页面可以快速加载,我们基于 PRPL 模式  进行优化。PRPL 是四个词的首字母缩写,分别代表:
    • Preload 预加载最重要的资源
    • Render 尽快渲染初始内容
    • Pre-cache 预缓存其他资源
    • Lazy load 懒加载其他路由和非关键资源
    • 首先,我们需要优化关键路径资源,页面中要呈现的内容很多,但不是所有内容都需要第一时间呈现,优先呈现最重要的内容。浏览器并不知道哪些资源是最重要的,基于 Resource Hints  可以告诉浏览器资源优先级。常用的有以下几类:
    • preconnect 启动早期连接,包括 DNS 查找,TCP 握手等
      • preload 预加载资源并缓存,以便需要时立即使用
      • prerender 标识下一步导航可能需要提示渲染页面
        • prefetch 预获取资源,优先级比 preload 低,浏览器自行判断合理时间执行操作

        3.3、React 性能优化

        组件懒加载
        可视区域之外的内容,和需要用户交互时才呈现的组件,都可采用懒加载,保证页面首要内容快速呈现。要做懒加载,首先需要合理定义拆分点进行代码分割,然后基于动态导入和 React.lazy 即可实现。
        对于大部分点击触发的组件来说,这样已经足够,但针对页面底部可视区域之外需常规滚动查看的内容,还要做一些额外的工作。可以自行封装实现一个组件,在内部进行判断内容是否可视,并监听 scroll 事件重新渲染。
         
        实际中,我们结合 react-lazyload 和 @loadable/component 实现所需功能,如下:
        同时,可对用户比较频繁使用的组件预加载,
         
        优化渲染方式
        目前渲染方式主要分为服务端渲染(SSR)和客户端渲染(CSR)两类大的方式:
        • 客户端渲染(CSR)的最大问题在于受用户所在的环境影响其实加载速度,主要表现在网络层脚本文件的加载和浏览器的执行效率,在不同环境差异会非常大。
        • 服务端渲染(SSR)能解决掉上述CSR的问题,通过服务端渲染,直出HTML内容让用户快速呈现页面的主要内容,可以很好改善FCP和LCP的指标
        最佳实践:
        1. 针对首屏采用服务端渲染,让用户更快看到内容,其他仍使用客户端渲染的模式,减轻服务器压力,毕竟将大量用户的渲染任务转移到服务端会是一笔不小的开销。这时,结合缓存机制可以大大节省渲染时间。
        1. 将首页渲染(这里是指 JavaScript 执行层面的)工作转移到服务端,毕竟服务端相对更可控,在首屏之前避免减少资源网络传输,从而减少耗时,因为网络是更不可控的一个因素
        1. 所以会引申出预渲染的概念,即基于构建时的预渲染,是使用 webpack 和 babel 等工具提前生成对应的 HTML 以及引用的脚步和样式文件和基于运行时的,使用 headless 浏览器。但预渲染并不适用于有大量动态内容的页面。
         
        合理使用不可变数据
        shouldComponenetUpdateuseMemo 和 useCallback函数可以根据实际场景使用。
        默认的 shouldComponenetUpdate总是返回 true ,但开发者知道什么时候应该更新,则可自行实现该生命周期方法。推荐大部分组件都使用 pureComponent代替,函数组件则可使用 Memo
         
        useMemo 和 useCallback 都是记忆函数,可结合 Memo 避免不必要的重新渲染,或者是对昂贵计算的记忆。
         
        state 和 props 都是不可变数据,在更新深层嵌套数据使用深拷贝不是一种好方式,可借助 Immer 这类库更好地编写。
        最后说明一点,在必要的时候进行性能优化,大部分时候无需考虑,而且滥用方法反而损害性能。
         

        3.4、优化长任务

        Long Task (长任务)的定义是执行时间超过 50 ms 的任务。我们知道,JavaScript 是单进程单线程的模型,主线程上一旦有耗时长的任务存在时,就会造成阻塞,无法响应用户输入。
        Long Task 跟 Lighthouse 中的两个重要性能指标 TTI 和 TBT 息息相关,而这两个指标占比为 40% ,可以说优化好 Long Task 能大幅提升页面性能。
        Long Task 可借助对应的 Long Task Web API 进行监控,开发过程中则使用 Chrome DevTools Performace 面板查看。需要注意的是,开发者的电脑配置可能很强,但用户尤其是移动端的用户环境并没有那么乐观,应该适当调低硬件配置和网络速度,这样能发现更多的 Long Task 。
         
        任务类型有多种,除了最常见的脚本执行之外,还包括脚本解析编译、HTML 解析、CSS 解析、布局、渲染等。脚本执行是长任务的主要表现形式,这里着重说明在 JavaScript 执行上的一些优化方式:
        • requestIdleCallback API
          • 针对一些不重要的任务比如埋点日志可以直接丢到 requestIdleCallback 中,浏览器会在空闲时间执行。在不支持的环境可使用 shim) ,基于 setTimeout 实现近似的功能。
            库 idlize 中封装了一些非常实用的帮助函数,使用这些方法可把任务延迟到需要的时候再执行。
        • Web Worker
          • 如果项目中确实存在比较复杂的计算,可启动 Web Worker 单独另开一个线程来计算,并使用 message通信。
        • 记忆函数
          • 如果一个函数被大量调用,合理运用记忆函数一个很好的选择,有大量的库可供我们选择,也可以根据使用场景自行实现。
        • Debounce和Throttle
          • 针对 input change 和 scroll 等可能频繁触发的事件,避免无节制地调用。
         

        3.5、减少布局偏移

        在开发调试中,Layout Shift 同样可以使用 Chrome DevTools Performance 进行分析,能查看每一次布局偏移的分数,进行针对性优化,有对应的 Layout Instability API可以帮助收集用户的布局偏移数据。
         
        常用的优化方案有:
        • 为动态元素预静态预留空间
        • 图片宽高尺寸固定
         
        预留空间可减少其他页面元素的偏移,比如出现在最顶部的广告位,在数据还未获取到的时候预先设置好一个容器,可避免后续大幅偏移。
         
        针对整页动态的内容,使用骨架屏是一种很好的模式,业界已有不少成熟方案可自动生成。
        设置图片宽高,则可以保证浏览器在加载图片过程中始终能分配正确的空间大小。