web性能及优化

*更新信息

  • 2022.01.23:更新端侧应用的性能优化手段;整理常用优化手段;

前言

用户在访问 web 网页时,大部分都希望网页能够在一秒完成。事实上,加载时间每多 1 秒,就会流失 7%的用户。如果时间超过 8s 就会感到不耐烦,如果加载需要太长时间,用户就会放弃访问。这也就是著名的 “8s 原则”。

虽然当今设备及网络环境都大幅提升,但“带宽低”、“速度慢”、“内存小”的平均情况仍然是明显性能瓶颈,特别是移动端的各种条件限制。

性能优化是一个非常庞大的工程,包括指标制定、标准确定、进行优化手段、进行性能测试、确定性能指标采集上报、确定性能监控及预警、评估收益等等。本文重点描述指标制定和优化手段。

一、如何评估网页性能

性能度量指标

我们都明白性能的重要性,但当我们谈起性能时,我们具体指的是什么?其实性能是相对的:

  • 由于网速或设备的差异,某个网站可能对一个用户来说速度很快,但可能对另一个用户来说速度很慢。
  • 两个网站完成加载所需的时间或许相同,但其中一个却显得加载速度更快(如果该网站逐步加载内容,而不是等到最后才一起显示)。
  • 一个网站可能看起来加载速度很快,但随后对用户交互的响应速度却很慢(或根本无响应)。

因此,在谈论性能时,重要的是做到精确,并且根据能够进行定量测量的客观标准来论及性能。这些标准就是指标。

  • FCP:First Contentful Paint,首次内容绘制,测量页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间。
  • FPM:First Meaningful Paint,首次有效绘制,当主要内容呈现在页面上;
  • HRT:Hero Rendering Times,英雄渲染时间,度量用户体验的新指标,当用户最关心的内容渲染完成;
  • TTI:Time to Interactive,可交互时间,指页面布局已经稳定,关键的页面字体是可见的,并且主进程可用于处理用户输入,基本上用户可以点击 UI 并与其交互;
    输入响应(Input responsiveness,界面响应用户输入所需的时间)
  • PSI:Perceptual Speed Index,感知速度指数,测量页面在加载过程中视觉上的变化速度,分数越低越好;
  • LCP:Largest Contentful Paint 最大内容绘制,测量页面从开始加载到最大文本块或图像元素在屏幕上完成渲染的时间。
  • FID:First Input Delay,首次输入延迟,测量从用户第一次与您的网站交互(例如当他们单击链接、点按按钮或使用由 JavaScript 驱动的自定义控件)直到浏览器实际能够对交互做出响应所经过的时间。
  • TBT:Total blocking time 总阻塞时间,测量 FCP 与 TTI 之间的总时间,这期间,主线程被阻塞的时间过长,无法作出输入响应。
  • CLS:Cumulative Layout Shift,累积布局偏移,测量页面在开始加载和其生命周期状态变为隐藏期间发生的所有意外布局偏移的累积分数。

注:FMP 与英雄渲染时间非常相似,但它们不一样的地方在于 FMP 不区分内容是否有用,不区分渲染出的内容是否是用户关心的。

但仅仅因为某个指标基于客观标准并且能够进行定量测量,也并不一定意味着那些测量值就是有用的。我们也需要自定义指标,由业务需求和用户体验来决定。

有时候,可能会有某个特定网站比较独树一帜,需要额外的指标来捕获完整的性能全貌。例如,LCP 指标用于测量页面的主要内容何时完成加载,但在某些情况下,最大元素并不是页面主要内容的一部分,因此 LCP 就不再是相关指标。

为了解决这种情况,Web 性能工作组还推出了一系列较低级别的标准化 API,可用于实现您自己的自定义指标:

RAIL 模型

RAIL 是 response (响应)、 animation(动画)、idle(浏览器空置状态)和 load(加载)。

从这四个模块角度来思考你的产品。如果在每个模块上,你都可以达到性能优化的目标值,那么最终用户感受到的将会是极致的体验。

RAIL 模型中,通常建议性能阈值:

  • Response:点击/轻触后 100 ms 内得到响应;
  • Animation:每一帧的渲染在 16 ms 内完成;拖拽后的页面渲染也要在 16 ms 内完成;
  • Idle:合理地使用浏览器空闲时间;任务要在 50 ms 内完成;
  • Page Load:加载的过程要满足“响应”目标;最重要的内容要在 1000 ms 内完成加载。

p-2-rail-model.png

在次性能目标/阈值基础上,结合 APM 统计,便能对线上用户的 RAIL 性能情况得到统计和评估。

开发者性能检测工具

很多工具能够帮助我们获取或分析上文中的一些通用性指标。

Chrome DevTools

Chrome 浏览器开发者工具中的LighthousePerformance能有效帮助我们进行性能分析:

  • Lighthouse:生成性能分析报告。并且给予相关优化建议;
  • Performace:分析运行时数据报告,包含阻塞、重排等细节信息;

p-1_chrome-devtools.jpg

根据这两个有效工具能够帮我们分析当前页面/运行的性能情况,以便有效做出优化。具体工具使用可以直接参考官网文档《Chrome-Analyze runtime performance》《Using Lighthouse To Improve Page Load Performance》

*lighthouse 本地使用

node12.x起,可以直接安装lighthouse在本地:

1
npm i -g lighthouse

简单分析:

1
lighthouse https://blog.michealwayne.cn

以此为基础,我们可以可以将分析工作自动化及平台化。比如https://web.dev/measure/

p-3_pagespeed.jpg

*React Profiler

React Profiler 是 React 官方提供的性能审查工具。React 16.5 添加了对新 DevTools 分析器插件的支持。该插件使用 React 的实验性 Profiler API 来收集有关渲染的每个组件的时间信息,以便识别 React 应用程序中的性能瓶颈。使用官网说明https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html

p-5_react-profiler.jpg

*通过 webpack-bundle-analyzer 和 vue-cli report 分析模块

通过webpack-bundle-analyzer,我们可以分析各个模块的大小,进行对性能影响最大的模块进行优化。vue-cli report 也集成了这功能。

p-6_webpack-report.jpg

真实的性能分析工具:WebPageTest

WebPageTest(地址https://www.webpagetest.org/),通过浏览器访问,基于输入的 WebSite URL,以及选择的国家城市、浏览器类型、网络带宽等信息,启动对应的远程服务器上的浏览器进行性能分析测试。相似的还有 YSlow、PageSpeed。

其他性能测试方案

使用性能 SDK

如 APM-SkyWalking client。优点是方案与到时候线上用户一致,缺点是在测试环节样本少,需要暴露到线上。

借助工具录视频

通过自动化工具进行视频录制,优点是可以和 QA 设施相结合,还能同时测试内存和 CPU 等信息。缺点是对于真实用户的监控力度不足。

借助端侧打点

与端侧能力相结合,通过像素检测等手段进行打点上报。优点是能兼顾线上用户及 QA 设施结合。缺点是只能作用在端内业务,无法检测端外业务。

二、性能优化原则

  • 数据驱动。依据数据而不是凭空猜测,性能指标需要满足可度量的条件,当我们怀疑性能有问题时,应该通过日志数据、测试和来分析。
  • 忌过早优化和过度优化。过早优化很容易拍脑袋,优化过程也要考虑性价比。
  • 深入理解业务。选择合适的衡量指标。性能优化服务于业务,业务服务于最终用户,关注以用户为中心的真实体验。
  • 持久战。性能优化也有渐进明细的规划和实施特征。
  • 正推&反推。结合正推和反推制定目标,正推:线索 -> 本质/痛点 -> 解决方案 -> 目标;反推:目标 -> 解决方案 -> 本质/痛点 -> 线索,综合制定目标。如 30%的性能提升,带来 10%的流量增加。

*业界性能标准

百度、美团等主要指标以 FCP、FMP 和卡顿率为主;阿里云 ARMS 性能监控方案,定义性能指数 Apdex,使用白屏时间作为计算指标。

业界较为通用的标准:

业务类别 较快 很慢 指标示例
时间敏感 <1s 1~1.5s 1.5~2.5s >2.5s 首屏/白屏
时间不敏感 <2s 2~4s 4~8s >2.5s onload

像 2C 的页面,“秒开”应作为性能目标,如:

类型 FCP 秒开率 1.5 秒开率 2 秒开率
全离线 700ms 90% 98% 99%
部分离线/SSG/SSR 1000ms 80% 95% 98%
端内 1200ms 70% 90% 95%
端外 1500ms 50% 60% 80%

三、通用性能优化手段

性能优化手段可以大致分为以下几个阶段:

p-main.png

设计阶段

好的前端模式设计能带来性能优化事半功倍的效果,目前主流的做法就是通过借助端侧或者服务端能力。

借助端侧能力

端侧业务可以借助客户端能力进行性能优化。常用的方式如下:

  • 离线:通过将在线请求的资源本地化提升加载性能
    • 通用资源离线。页面及业务代码在线,通用资源走端侧离线,适用运营活动等流动页面。
    • 页面整体离线。页面相关静态资源基本全部离线,适用于强性能、稳定性要求的页面。
  • 混合/原生:借助端侧能力降低渲染成本
    • 原生组件。弹窗、toast、Video 等控件原生调用。
    • 原生渲染。通过 DSL 控制原生呈现、RN、小程序等。

借助服务端能力

意义在于弥补主要内容在前端渲染的成本,提升首次有效绘制的速度。可以使用服务端渲染来获得更快的首次有效绘制。比较推荐的通用做法是:使用服务端渲染静态 HTML 来获得更快的首次有效绘制,一旦 js 加载完毕再将页面接管下来。

  • SSG。通常需要业务定制,不过也有像 JAMStack 等相对通用型框架;也包括 PHP、Java 等实现;
  • SSR。Nextjs、Nustjs 等,也包括 PHP、Java 等实现。
  • 微前端。如 qiankun 等基于 single-spa 的系列、fronts 等基于 module-federation 的系列。
  • *bigPipe:一种动态网页加载技术。将网页分解成称为 pagelets 的小块,然后分块传输到浏览器端,进行渲染。

借助服务端能力一定要注意“三高”的控制及监控。

前端框架选择

从性能角度考虑:

  • 纯 web:性能最佳,开发成本相对较大,适合极高性能要求的单数据流业务场景,如首页。模板渲染可使用Mustache
  • Vue:开发成本最小。可考虑使用petite-vue等 lite 库减负。
  • React:开发成本小。可考虑使用preact等 lite 库减负。
  • 其他:像Svelte其实也比较轻,但目前沉淀少,开发成本较大。

开发阶段

静态资源准备工作

  • 图片格式选择:丰富色彩使用 jpg、透明色使用 png、单调色彩使用 2/4/8/16/32/64/128/256 位 png(每个通道 8 位为每个通道提供 256 个值,RGB 三个通道一共可以为每个像素提供 16777216 种颜色。);根据环境使用 WebP;减少 gif 的使用,使用 CSS3/SVG/canvas 动画、lottie 等新形式,一些场景下视频也比 gif 效率高。
  • 响应式图片。srcsetsizes<picture>元素使用响应式图片。还可以通过<picture>元素使用 WebP 格式的图像。
  • CSS3/SVG 代替图片,SVG 也可以通过svgo等工具进行压缩优化。
  • 使用字体图标代替图片图标
    p-4_img-optimize.png

编码阶段

  • HTML

    • 正确的标签写法。当 HTML 标签不满足 web 语义化时,浏览器需要更多时间去解析 DOM 标签的含义,进行语法纠错。
    • 减少 iframes 使用,或者延迟 iframes 加载,保证不会影响父文档加载。
    • 删除无效内容,减少 DOM 节点。
    • 删除元素默认属性。
    • 避免节点深层级嵌套,避免生成 dom 树时占用太对内存。
    • 减少 table 布局,table 开销非常大。
    • css、js 尽量外链,js 考虑异步引入。
  • CSS 优化

    • 降低 CSS 选择器复杂性。避免使用通用选择器*;选择器层级不宜过多;提高关键选择器(最右)的匹配强度,比如不适用元素选择器;谨慎使用一些expensive的属性,如nth-child
    • 避免浮动布局,使用 flexbox
    • 善用 will-change 属性:will-change 属性告诉浏览器元素的哪些属性需要修改,使浏览器能够在元素实际更改之前设置优化,通过在实际更改前执行耗时的工作以提升性能。
    • 善用 font-display 属性优化字体:font-display 属性定义了浏览器如何加载和显示字体文件,允许文本在字体加载或加载失败时显示回退字体。可以通过依靠折中无样式文本闪现使文本可见替代白屏来提高性能。
    • 善用 contain 属性:contain 属性允许开发者指示元素及其内容尽可能独立于文档树的其余部分。 这允许浏览器针对 DOM 的有限区域而不是整个页面重新计算布局/样式。
  • JS 优化

    • 使用原生方法。
    • 异步化。任务冲突时(主线程和合成线程调度不合理)使用requestAnimatinFrame(..)/setTimeout(..)分割队列;Web Worker 等。
    • 减少重绘重排
      • 对高频触发的事件进行节流或防抖。
      • 减少 DOM 操作,批量操作 DOM:请尽可能减少访问 DOM 的次数(缓存 DOM 属性和元素、把 DOM 集合的长度缓存到变量中并在迭代中使用);如果操作需要进行多次重排与重绘,建议先让元素脱离文档流;善于使用事件委托。
    • 考虑使用事件委托
    • 循环/运算优化
      • switchif...else 的使用
      • 善用位运算
    • 流程控制优化。一些流程控制相关的一些做法可以略微提升性能(这些细节在大型开源项目中大量运用):避免使用 for...in(它能枚举到原型,所以很慢);在 js 中倒序循环会略微提升性能;减少迭代的次数;基于循环的迭代比基于函数的迭代快;用 Map 表代替大量的 if-elseswitch等。
    • 数据读取优化。如字面量与局部变量的访问速度最快,数组元素和对象成员相对较慢;变量从局部作用域到全局作用域的搜索过程越长速度越慢;对象嵌套的越深,读取速度就越慢;对象在原型链中存在的位置越深,找到它的速度就越慢;
    • 配合 V8 等引擎的优化。如 JIT、Hidden Class、函数的解析方式有 lazy parsing 懒解析和 eager parsing 饥饿解析
  • vue

    • 根据场景使用 no-reactive data
    • 不要将所有数据都放到 data 中
    • 尝试 JSX/TSX 写法
    • 善用 Functional component
    • 子组件拆分
    • 使用局部变量
    • 根据业务场景使用 v-show
    • keep-alive 缓存组件
    • 列表使用 key,且不建议使用索引
    • 使用事件代理
    • 虚拟列表
    • 组件按需加载
    • 去除无用的路由、状态管理引用
  • React

    • 善用 Functional Component;
    • 善用 React.memo、PureComponent/shouldComponentUpdate
    • 善用 useMemo、useCallback
    • useEffect 设置合理的依赖项
    • 批量更新,unstable_batchedUpdates
    • 发布订阅者跳过中间组件 Render、跳过回调函数改变触发的 Render
    • 状态下放,缩小状态影响范围
    • 避免在 didMount、didUpdate 中更新组件 state
    • 子组件拆分
    • 使用局部变量
    • css 控制节前展示隐藏
    • 列表使用 key,且不建议使用索引
    • 使用事件代理
    • 虚拟列表
    • 组件按需加载
    • 去除无用的路由、状态管理引用

构建阶段

  • 构建优化

    • 提取三方库代码。如使用html-webpack-externals-plugin
    • Webpack 按需引入。
    • 使用动态引入。
    • Tree Shaking、Scope hoisting、Code-splitting。Webpack 与 Rollup 都支持 Scope Hoisting。code-splitting 能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。
  • 压缩文件。webpack 插件(webpack4 默认optimization.minimize配置):

    • js:UglifyPlugin
    • css:MiniCssExtractPlugin、clean-css
    • html:HtmlWebpackPlugin、html-minifier
    • 图片:tinypng、image-webpack-loader
  • 预渲染。根据业务场景,类似 SSG 方案将主要内容渲染至 html,如使用prerender-spa-plugin插件。

  • 根据资源优先级和阻塞情况,优先加载关键资源。

    • 将 CSS 文件放在 HTML 头部,js 文件放在 HTML 底部。
    • 按需异步加载 js:acync、defer、动态脚本。
    • 懒加载。延迟加载所有体积较大的组件、字体、JS、视频或 Iframe。可以通过 Intersection Observer 延迟加载图片、视频、广告脚本、或任何其他资源。可以先加载低质量或模糊的图片,当图片加载完毕后再使用完整版图片替换它。
    • 优先加载关键的 CSS。可以将首屏渲染必须用到的 CSS 提取出来内嵌到<head>中,然后再将剩余部分的 CSS 用异步的方式加载。可以借助工具critical
    • 利用资源提示 Resource hintsdns-prefetchpreconnectprefetchprerender、这些可以帮助浏览器决定应该连接到哪些源,以及应该获取与预处理哪些资源来提升页面性能。
    • 利用预加载 Preload,Preload 提供了预获取资源的能力,可以将获取资源的行为从资源执行中分离出来。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- 通过声明性标记预加载 CSS 资源 -->
    <link rel="preload" href="https://undefined.cn/moo-css-base.min.css" as="style" />

    <!-- 或,通过JavaScript预加载 CSS 资源 -->
    <script>
    const res = document.createElement('link');
    res.rel = 'preload';
    res.as = 'style';
    res.href = 'https://undefined.cn/moo-css-base.min.css';
    document.head.appendChild(res);
    </script>

PRPL 描述了一种用于使网页加载并变得更快的交互模式:Push (or preload):推送或预加载最重要的资源;Render:尽快渲染初始路径;Pre-cache:预缓存剩余资源;Lazy load:延迟加载其他路由和非关键资源。

p-load_flow.png

请求阶段

DNS

  • DNS 缓存优化;
  • 预解析 DNS,减少 DNS 查找。关于 DNS 的预解析可以参考《MDN-dns-prefetch》
1
<meta http-equiv="x-dns-prefetch-control" content="on" /> <link rel="dns-prefetch" href="https://blog.michealwayne.cn" />
  • 页面中资源的域名的合理分配,涉及域名不要太多(通常不要超过 6 个);

HTTP(s) 请求

  • 减少请求

    • 资源合并。根据复用情况使用s.thsi.cncb拼接;
  • 使用 HTTP2。相比 HTTP1.1,HTTP2 解析速度更快,也提供了部首压缩功能。更为重要的,多路复用实现并行能力。当前*.thsi.cn具备了 HTTP2 能力,因此静态资源应尽可能得部署在 thsi。

  • 善用缓存,不重复加载相同的静态资源。正确设置 expirescache-control 和其他 HTTP 缓存头。

    • 强缓存。如长期不变的资源设置Cache-Control:max-age=31536000
    • 协商缓存。资源可能随时发生变动。
  • 静态资源使用 CDN。借助边缘能力,静态资源尽可能放*.thsi.cn

  • 服务端启动 gzip

  • 避免重定向。服务端的 302、<meta>标签实现的重定向以及前端location.replace(..)的重定向都会引发新的 DNS 查询。

  • 懒加载。考虑请求优先级,非首屏图片/资源考虑延迟加载。

  • 离线化。见下文

渲染阶段

  • 减少渲染成本

    • 控制首次渲染节点及样式,不易过多。
    • 简单图形以 CSS 呈现。
    • 避免首次渲染时的重绘重排。
  • 预渲染。见下文

  • 骨架屏。可以自己手动写骨架,也可以通过一些基于 puppeteer 的骨架生成方案。

运行阶段

  • 减少重绘重排

    • 对高频触发的事件进行节流或防抖
    • 减少 DOM 操作,批量操作 DOM
  • 动画

    • 在 GPU 上呈现动画,适时开启硬件加速。属性包括 3D transforms (transform: translateZ(), rotate3d()等),animating transformopacity, position: fixedwill-change,和 filter。如 <video>, <canvas><iframe>,也位于各自的图层上。 将元素提升为图层(也称为合成)时,动画转换属性将在 GPU 中完成,从而改善性能,尤其是在移动设备上。虽然一些像 GSAPVelocityJs 等一些 js 动画库声称性能上可以做得比 CSS 好,但目前绝大多数业务场景用不到这些库;反观 CSS3,简单的transform便能减少 reflow。
    • 较复杂动画中,用 lottie/视频 代替 gif。
    • requestAnimationFrame 实现视觉变化。
  • 交互

    • 快速响应的用户界面。PSI 是提升用户体验的重要指标,可以多使用一些骨架或 Loading 过渡。比如一些游戏类场景,可以先通过 loading 进度条保证主要资源提前加载或处理完成,带来运行时的体验提升。
    • 避免阻塞性执行,保证输入响应(Input responsiveness),如输入框的节流处理等。

p-7_ux-optimize.png

其他

  • canvas 性能优化

    • 离屏
    • 图像缓存
  • web worker

    • 应用场景:编码/解码大字符串;复杂数学运算(包括图像,视屏处理);大数组排序;
  • PWA:Service Worker & Offline data
    • 应用场景:端外复杂 web 应用,可见https://web.dev/learn/pwa/。注意 PWA 的缓存控制以及它不解决首次启动问题。

4.端侧应用的性能优化手段(hybrid)

借助端侧能力,hybrid 的性能优化效果远大于普通的处理优化,目前在主流应用中广泛应用。

端侧 Web 运行环境优化

  • 内核升级。iOS 升级 WKWebView、Android 升级 X5。

  • 协议及 WebView 启动优化,如全局化 WebView。

离线化

离线化可以最大程度地摆脱网络环境对 web 的影响。基本运作是将项目的静态资源打成离线包,客户端在启动应用等时机进行加载及解压操作。在访问资源时,客户端根据 URL 标识进行拦截,如果能在本地找到对应资源文件,则直接以本地信息响应,否则加载远程页面。

  • 优点:避免资源请求,首开性能极佳(全离线 FCP 通常能保证在 800ms 内);
  • 缺点:维护成本较高;
  • 适用场景:迭代频率相对稳定、高加载性能要求的业务场景。

离线包方案

需要与客户端配合,方案可以借鉴阿里mPaaS——H5 容器和离线包

webview 预热

在应用启动等特定触发时机下,在后台无感知启动 webview 并预热页面,此 webview 全局化、池化,在访问 URL 时即直接调起预热 webview。

  • 优点:访问是统一 webview ,加载体验最佳(外界最佳实践 200ms);
  • 缺点:有性能开销(空间换时间);仅能使用于业务最重要业务场景;
  • 适用场景:如资讯、双十一活动等高频业务。

性能要求极强的场景甚至可以考虑 NSR

WebView 资源预加载

在应用启动时,在后台无感知启动 WebView,此 WebView 通过配置项预先加载应用内常用 js/css/image/font 资源,以在访问页面时资源请求命中本地缓存。

  • 优点:不影响业务开发,成本小;
  • 缺点:通用资源的定义和配置需要评估、避免请求浪费;
  • 适用场景:应用存在高频公共资源,但这些资源无法使用离线方案。

通用预加载方案接入

可借助通用预热方案,启动预热 WebView,此 WebView 根据配置进行资源预加载。

当然,无论是离线还是预热,我们仍然需要考虑资源优化,比如通过分包、图片远程等方式,减少不必要的资源加载。


相关文档