浏览器性能优化及其相关指标的测量

360 阅读24分钟

如何测试速度

Lab data: 使用工具在稳定、受控的环境中模拟页面加载 developer.chrome.com/docs/lighth…

Field data: 基于真实用户的实际页面加载与页面交互 pagespeed.web.dev/

other tools developer.chrome.com/docs/crux/ developer.chrome.com/docs/devtoo…

关于如何测试field data, 后续篇章有专门的文章展开接受.需要时可以去瞅瞅 web.dev/fast/#intro…

web指标

很多.了解核心web指标即可,不需要成为性能指标专家

核心web指标

核心指标集合会随着时间推移而发展 每一个核心指标代表用户体验的一个不同方面,能够进行实际测量,并且反映出以用户为中心的关键结果的真实体验 2020 年的指标构成侧重于用户体验的三个方面——加载性能、交互性和视觉稳定性

Largest Contentful Paint (LCP) :最大内容绘制,测量加载性能 First Input Delay (FID) :首次输入延迟,测量交互性 Cumulative Layout Shift (CLS) :累积布局偏移,测量视觉稳定性

在JS中测量LCP: web.dev/lcp/#measur…
在JS中测量FID:web.dev/fid/#measur…
使用web-vitals库测量每一项指标:github.com/GoogleChrom…

FID无法在实验室环境中进行模拟 TBT(Total Blocking Time)的改进通常能够带来FID的相应改进 虽然FID和TBT测量内容不同, 可以在实验室测量TBT, 用于预测FID

自定义指标

有效的性能测量的第一条规则是确保性能测量技术本身不会导致性能问题。

例如,运行 requestAnimationFrame 循环并计算相邻帧之间的增量时间,来确定主线程是否由于长时间运行 JavaScript 任务而被阻塞.

因此,对于在网站上测量的任何自定义指标,如果可能,最好使用PerformanceObserver系列API

  1. User Timing API
performance.mark('myTask:start');
await doMyTask();
performance.mark('myTask:end');
performance.measure('myTask', 'myTask:start', 'myTask:end');
  1. Long Tasks AP 该 API 将报告执行时间超过 50 毫秒 (ms) 的任何任务
//po PerformanceObserver实例
po.observe({type: 'longtask', buffered: true});
  1. 其他 差不多都是基于PerformanceObserver实现的,很多啦 Element Timing API Event Timing API ....

针对核心指标进行优化

优化LCP

提高服务器的响应速度

  1. 优化您的服务器 服务器并不会在浏览器请求时发送一个已经准备好的完整 HTML 文件,而是需要运行逻辑来构建页面.
  • 减少数据库查询数据的时间
  • 对服务端使用的框架进行性能优化.可参考对应的性能优化指南
  1. 将用户路由到附近的 CDN emm...多花点钱搞多个CDN服务器.别把网页内容只托管到一个服务器上
  2. 缓存资源asserts 如果HTML是静态的, 缓存可避免不必要的重建, 最大限度减少服务器资源消耗 有多种方法可以进行服务器缓存
  • 配置反向代理(Varnish、nginx)来提供缓存内容
  • 配置和管理您的云服务提供商(Firebase、AWS、Azure)的缓存行为
  • 使用提供边缘服务器的 CDN,以便将您的内容进行缓存并存储在离您的用户更近的地方
  1. 优先使用缓存提供 HTML 页面 使用Service Worker提供较小的HTML响应.借助Workbox库实现更简单 从而避免不必要的请求
  2. 尽早建立第三方连接 使用rel="preconnect"来告知浏览器您的页面打算尽快建立连接 还可以使用dns-prefetch来更快地完成 DNS 查找

对于不支持preconnect的浏览器,可以考虑将dns-prefetch做为后备

<link rel="preconnect" href="https://example.com" />
<link rel="dns-prefetch" href="https://example.com" />
  1. 使用签名交换 签名交换 (SXG) 是一种交付机制,通过提供采用了易于缓存格式的内容来实现更快的用户体验 对于通过 Google 搜索获得大部分流量的网站,SXG 可以是改进 LCP 的重要工具

Google做的优化, 网站所有者不可控

JavaScript 和 CSS 渲染阻塞

浏览器在能够渲染任何内容之前,需要将 HTML 标记解析为 DOM 树。如果 HTML 解析器遇到任何外部样式表()同步 JavaScript 标签( ,则会暂停解析。 延迟加载任何非关键的 JavaScript 和 CSS,从而提高网页主要内容的加载速度

详情见CSS和JS文件优化

减少 CSS 阻塞时间

  1. 削减 CSS 压缩CSS内容(移除空格等等), 减少CSS文件个数. 这些,都可以借助构建工具实现
  2. 延迟加载非关键 CSS 使用 Chrome 开发者工具中的代码覆盖率选项卡查找您网页上任何未使用的 CSS
  • 如果是在您网站的单独页面上使用,可以将所有未使用的 CSS 完全删除或移动到另一个样式表
  • 对于任何初始渲染时不需要的 CSS,请使用 loadCSS 来异步加载文件
<!--rel="preload"和onload用于异步加载CSS文件-->
<link rel="preload" href="stylesheet.css" as="style" onload="this.rel='stylesheet'">
  1. 内联关键 CSS 将重要样式进行内联后,就不再需要通过往返请求来获取关键 CSS
<head>
  <!-- ... -->
  <style>
    <!-- 关键CSS在此! -->
  </style>
  <!-- ... -->
</head>

减少JavaScript阻塞时间

减少JS文件请求导致的阻塞可以通过优化网络,压缩文件等方式缓解 减少JS文件编译解析导致的阻塞可以通过文件使用方式缓解

  1. 削减和压缩 JavaScript 文件 削减:删除空格和不需要的代码 压缩: 存在于服务器和客户端交互
  2. 延迟加载未使用的 JavaScript 拆分 JavaScript 包, 在用户加载应用程序时仅发送初始路由所需的代码 最大限度地减少需要解析和编译的脚本数量,可以加快页面加载时间
  3. 按需加载polyfill代码 现代浏览器不需要polyfill, 可以减少请求,编译解析相关代码导致的性能消耗

资源加载时间

虽然 CSS 或 JavaScript 阻塞时间的增加会直接导致性能下降,但加载许多其他类型资源所需的时间也会影响绘制时间。 影响 LCP 的元素类型为:

  • 元素
  • 内嵌在元素内的元素
  • 元素(使用封面图像测量 LCP)
  • 通过url()函数(而非使用 CSS 渐变)加载的带有背景图像的元素
  • 包含文本节点或其他行内级文本元素的块级元素

确保影响LCP的元素尽快加载完成的方法

  1. 优化和压缩图像
  • 首先考虑不使用图像。如果图像与内容无关,请将其删除。
  • 压缩图像(例如使用 Imagemin)
  • 将图像转换为更新的格式(JPEG 2000、JPEG XR 或 WebP)
  • 使用响应式图像
  • 考虑使用图像 CDN
  1. 预加载重要资源 多种类型的资源都可以进行预加载,但您应该首先侧重于预加载关键资产,例如字体、首屏图像或视频,以及关键路径 CSS 或 JavaScript 预加载与响应式图像一起使用,可实现更快速的图像加载
<link rel="preload" as="script" href="script.js" />
<link rel="preload" as="style" href="style.css" />
<link rel="preload" as="image" href="img.png" />
<link rel="preload" as="video" href="vid.webm" type="video/webm" />
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin />
<link
  rel="preload"
  as="image"
  href="wolf.jpg"
  imagesrcset="wolf_400px.jpg 400w, wolf_800px.jpg 800w, wolf_1600px.jpg 1600w"
  imagesizes="50vw"
/>
  1. 压缩文本文件 压缩可以显著缩减在服务器和浏览器之间传输的文本文件(HTML、CSS、JavaScript)大小 压缩您的资源将最大限度地减少这些资源的交付大小、缩短加载时间,从而改善 LCP。 大多数托管平台、CDN 和反向代理服务器在默认情况下都会对资产进行压缩编码,或者使您能够轻松配置资产。 推荐使用Brotli压缩算法, 在构建过程中压缩资源
  2. 基于网络连接交付不同资产(自适应服务) 根据用户的设备或网络条件(网络状况 API、设备内存 API 和硬件并发 API )按需获取不同的资源 例如:对于任何低于 4G 的连接速度,您可以显示图像,而不是视频:
if (navigator.connection && navigator.connection.effectiveType) {
  if (navigator.connection.effectiveType === '4g') {
    // 加载视频
  } else {
    // 加载图像
  }
}
可能用到的一系列实用属性
navigator.connection.effectiveType:有效连接类型
navigator.connection.saveData:启用/禁用数据保护程序
navigator.hardwareConcurrency:CPU 核心数
navigator.deviceMemory:设备内存
  1. 使用 Service Worker 缓存资产 Service Worker 可用于完成许多有用的任务.可借助Workbox库更新预缓存资产
  • 提供较小的 HTML 响应(前文有提及)
  • 缓存任何静态资源,并在收到重复请求时将资源直接提供给浏览器,而无需通过网络。

客户端渲染

如果您正在搭建一个主要在客户端进行渲染的网站,那么您应该避免使用大型 JavaScript 包。 如果您没有通过优化来加以阻止,那么在所有关键 JavaScript 完成下载和执行前,用户可能都无法看到页面上的任何内容或与之交互。

  1. 最小化关键 JavaScript 优化方向同减少JavaScript阻塞时间
  2. 结合服务端渲染
  3. 使用预渲染 预渲染是一种独立的技巧,比服务端渲染简单. 借助无头浏览器(没有用户界面的浏览器)在构建阶段生成每个路由的静态 HTML 文件。然后将这些文件与应用程序所需的 JavaScript 包一起进行传送

优化FID

FID 主要是由繁重的 JavaScript 执行导致的 当主线程繁忙(忙于执行JS),浏览器无法对用户交互做出响应 优化您网页上 JavaScript 的解析、编译和执行方式将直接降低 FID

分割长任务

任何阻塞主线程 50 毫秒或以上的代码都可以被称为长任务。长任务是潜在 JavaScript 膨胀的标志(加载和执行的内容超出用户现在可能需要的范围) 将长时间运行的代码拆解为更小的异步任务, 可以减少输入延迟

RAIL模型建议在50ms内处理用户输入事件,以确保在100ms内做出可见响应 若不这么做,会中断操作和响应之间的连接

做好交互准备

  1. 第一方脚本执行会延迟交互准备 渐进式加载代码有助于分散工作量,改善交互准备 考虑将更多逻辑转移到服务器端,或在构建期间静态生成更多内容
  2. 数据获取会影响交互准备的许多方面 级联获取数据会影响交互延迟,因最大限度减少对级联获取数据的依赖 最大限度减少客户端进行后处理的数据量
  3. 第三方脚本执行也会加剧交互延迟 按需加载第三方代码

使用 Web Worker

Web Worker能够让 JavaScript 在后台线程上运行, 从而避免阻塞主线程

详情见JS执行环境选择部分

减少 JavaScript 执行时间

  1. 延迟加载未使用的 JavaScript 需要利用好代码分割, 使用async,defer延迟加载任何非关键JS代码/文件
  2. 按需导入polyfill相关代码
  3. 减少第三方代码的影响(优化第三方资源)
  4. 减少 JavaScript 执行时间
  5. 使用Web Worker, 最小化主线程工作
  6. 保持较低的请求数(详情见网络优化)和较小的传输大小

优化CLS

导致CLS较差的常见原因有

  • 无尺寸的图像
  • 无尺寸的广告、嵌入和 iframe
  • 动态注入的内容
  • 导致不可见文本闪烁 (FOIT)/无样式文本闪烁 (FOUT) 的网络字体
  • 在更新 DOM 之前等待网络响应的操作
  • 动画

针对无尺寸的图像

始终在您的图像和视频元素上包含width和height属性。 或者通过使用 CSS 长宽比容器 预留所需的空间

所有浏览器的UA样式表 都会在图像加载前 根据元素的width,height属性 添加默认长宽比 图片加载后, 可根据被设定的宽度(或高度)和长宽比, 自动计算高度 如果图片没有设置宽高, 将拉伸以填满的宽高.

请求图片资源的长宽比可能和预留的不一样,为防止图片拉伸,可设置CSSobject-fit:cover

处理响应式图像(不同设备提供不同图像) 最流行的2个图像调整工具: sharp npm package, ImageMagick CLI 也可以尝试图像服务Thumbor(开源),Cloudinary

<!--Resolution switching: Different sizes-->
<img
  srcset="elva-fairy-480w.jpg 480w, elva-fairy-800w.jpg 800w"
  sizes="(max-width: 600px) 480px,
         800px"
  src="elva-fairy-800w.jpg"
  alt="Elva dressed as a fairy" />
  
<!--Resolution switching: Same size, different resolutions-->
<img
  srcset="elva-fairy-320w.jpg, elva-fairy-480w.jpg 1.5x, elva-fairy-640w.jpg 2x"
  src="elva-fairy-640w.jpg"
  alt="Elva dressed as a fairy" /><!--大屏小屏提供不同布局的图片-->
<picture>
  <source media="(max-width: 799px)" srcset="elva-480w-close-portrait.jpg" />
  <source media="(min-width: 800px)" srcset="elva-800w.jpg" />
  <img src="elva-800w.jpg" alt="Chris standing up holding his daughter Elva" />
</picture>

针对无尺寸的广告、嵌入和 iframe

针对动态注入的内容

通过使用占位符或后备回调符为嵌入预先计算足够的空间 使用占位符或是骨架屏

针对导致不可见文本闪烁 (FOIT)/无样式文本闪烁 (FOUT) 的网络字体

当网页上的资源动态变化时,会发生布局偏移或重新布局,从而导致内容“偏移”。

  • 方案1: 合理配置font-display, 搭配size-adjust属性消除字体切换导致的网页位置偏移. 将和font-display: optional结合使用可以解决大部分问题
  • 方案2: 刚开始加载页面前使用系统字体(保证文本一定被显示),监测自定义字体的加载(可借助FontFaceObserver库), 字体加载完成后重构页面样式
font-display特点
optional性能最好. 如果100ms内字体还不可用,使用备用字体.可用了也不会进行切换
swap文字显示没有延迟.可用直接用选择的字体.不可用就先用备用字体,啥时候web fonts可用了,将备用字体换成web fonts. 可能会导致页面偏移
block一直等待web fonts可用, 使用web fonts

针对动画

倾向于选择transform动画,而不是触发布局偏移的属性动画 合适使用will-change属性(可以产生新的一层layout)

查看属性是否触发重排: csstriggers.com/

Babel配置

Babel功能: 将"现代"JS编译成"传统"JS, 或是能在Node中运行的版本 为了只编译用户需要的代码,你需要先确定哪些浏览器是你想兼容的

  • 使用@babel/preset-env, 最大范围覆盖你想兼容的浏览器及其系列版本
  • 使用<script type="module">取消发送polyfill代码给不需要的浏览器
//.babelrc
{
 "presets": [
   [
     "@babel/preset-env",
     {
       "targets": {
         "esmodules": true
       }
       "bugfixes": true //Babel 7.9, 精简不需要的polyfill
     }
   ]
 ]
}

<script type="module"> 默认就是defer的 使用模块/非模块模式交付2个单独的资源包, 仅将polyfill用于真正需要的浏览器

  <script type="module" src="main.mjs"></script>
  <script nomodule src="compiled.js" defer></script>

JS文件资源优化

针对JS文件代码的优化

事件相关(通常是用户交互相关)的代码

在event handler中做尽量少的工作, 尽量保持每个task工作量小一些 保持每个task工作量小,具体实现思路和优化长任务差不多

优化长任务(减小单个js任务执行时间)

大体思路就是任务拆分. 具体到任务拆分,便是八仙过海,各显神通了

  • 通过拆分放到微任务队列中执行. 缺点是不一定能拆出合适的部分
  • 通过async/await 退出主进程. 执行完一个小任务就主动退出,后续继续执行. 牺牲一点性能换取响应性.
  • 只有存在用户交互,才主动退出.牺牲更少的性能换取高响应
  • 不重要的逻辑,不影响渲染结果的逻辑放到下一帧执行

长任务不拆分成短任务.png

长任务拆分成短任务.png

长任务短任务对用户交互的影响.png

将部分任务拆分, 放到微任务队列中执行

常用的API有: setTimeout(cb,0),postMessage(),requestIdleCallback()

使用async/await退出主进程

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}
async function saveSettings(){
  const tasks=[task1,task2,...,task5];
  while(tasks.length){
    const task=task.shift();
    task();
    await yeildToMain();
  }
}

Yielding to the main thread creates opportunities for critical work to run sooner.

必要时退出主线程

只有监测到用户尝试进行交互时,才找机会退出主线程 isInputPending()返回true, 说明用户尝试交互

用户交互发生之后, isInputPending()需要时间反应 因此, 即使使用了isInputPending, 每个小任务执行时间也应该有所限制,不宜过长

async function saveSettings(){
  const tasks=[task1,task2,...,task5];
  while(tasks.length>0){
    if(navigator.scheduling.isInputPending()){
      await yieldToMain();
    }else{
      const task=task.shift();
      task();
    }
  }
}
//若浏览器不支持isInputPending, 使用50ms的时间间隔分割长任务
async function saveSettings(){
  const tasks=[task1,task2,...,task5];
  let deadline=performance.now()+50;
  while(tasks.length>0){
    if(navigator.scheduling?.isInputPending() || 
       performance.now()>deadline){
      await yieldToMain();
      deadline=performance.now()+50;
      continue;
    }
    const task=tasks.shift();
    task();
  }
}

长任务拆分改进

以上拆分长任务的方法美中不足的是: 拆分后的任务会放到队列最末尾执行 可以借助postTask安排任务优先级.需要注意的是, postTask不兼容所有浏览器

postTask() API的3个优先级

  • background 低优先级任务
  • user-visible 中等优先级任务,默认选项
  • user-blocking 高优先级任务
function saveSettings () {
  // Validate the form at high priority
  scheduler.postTask(validateForm, {priority: 'user-blocking'});
  scheduler.postTask(saveToDatabase, {priority: 'background'}); 
}

非重要逻辑下一帧执行

模拟浏览器的事件循环机制 requestAnimationFrame在事件循环的渲染阶段执行,必须在下一帧展示前执行完毕

推荐使用scheduler API

非重要逻辑下一帧执行.svg

//以带字数统计和拼写检查的文本编辑框为例
textBox.addEventListener('keydown', (keyboardEvent) => {
  // 立即根据用户输入更新UI
  updateTextBox(keyboardEvent);

  // 其他工作在下一帧执行
  requestAnimationFrame(() => {
    setTimeout(() => {
      const text = textBox.textContent;
      updateWordCount(text);
      checkSpelling(text);
      saveChanges(text);
    }, 0);
  });
});

针对 JS文件 文件本身的优化

  1. 代码分割, 按需导入. 初始页面不需要的,延迟加载 需要利用好代码分割, 使用async,defer延迟加载任何非关键JS代码/文件 很多打包工具都支持单独打包动态导入的文件
form.addEventListener("submit", e => {
  e.preventDefault();
  import('library.moduleA')
    .then(module => module.default) // using the default export
    .then(someFunction())
    .catch(handleError());
});
  1. 移除未使用,不需要的代码 使用tree-shaking插件移除没用到的代码 使用tree-shaking友好的第三方库/框架
  2. 构建时压缩代码,传输时/传输前压缩代码数据 构建时,借助Terser删除空格和不需要的代码 推荐使用Brotli压缩算法, 构建时静态压缩待传输代码 压缩方式
  • 动态压缩:浏览器请求时压缩.比构建过程压缩文件简单,但高压缩级别会导致延迟
  • 静态压缩:构建时压缩.构建时间长,使用高级别压缩尤甚.但可确保浏览器获取资源时不出现延迟
  1. 按需导入polyfill代码 为了只编译用户需要的代码,你需要先确定哪些浏览器是你想兼容的
  • 使用@babel/preset-env, 最大范围覆盖你想兼容的浏览器及其系列版本
  • 使用<script type="module">取消发送polyfill代码给不需要的浏览器

借助插件(Optimize Plugin等),为现代浏览器和旧版浏览器生成单独的打包文件,浏览器按需导入polyfill代码(使用模块/无模块的模式加载)

  <script type="module" src="main.mjs"></script>
  <script nomodule src="compiled.js" defer></script>
  1. 避免依赖CommonJS模块,整个应用程序中只使用ECMAScript模块语法 CommonJS 是 2009 年的标准,它为 JavaScript 模块建立了约定规范。它最初并没有打算用在 Web 浏览器上,主要用于服务器端应用程序。 服务器端 JavaScript 应用程序的大小并不像在浏览器中那么重要,这就是为什么 CommonJS 在设计时没有考虑减少生产包的大小。 ES 模块中的导入位置始终是字符串,而 CommonJS 可以是表达式.因此, CommonJS 模块动态程度更高, 更难优化。
  2. 使用PRPL模式实现即时加载
  • Push: 推送/预加载关键资源 预加载适合浏览器较晚发现的关键资源 浏览器会缓存预加载的资源,以便在需要时可以立即使用(不会执行脚本或应用样式表) 预加载资源时,可以指定type.只有浏览器支持该类型资源时,才会被预加载.否者会被忽略

预加载全部资源会适得其反.请谨慎使用

<!--as属性可以是script,style,font,image...-->
<link rel="preload" as="style" href="css/style.css">
<link rel="preload" as="script" href="critical.js">

<!--对于以匿名模式加载的字体资源,需要设置crossorigin属性.否者会被加载2次-->
<link rel="preload" href="ComicSans.woff2" as="font" type="font/woff2" crossorigin>
  • Render:尽快渲染初始路线 若使用客户端渲染, 内联关键 JavaScript , 并使用 async 推迟其余部分,以及内联首屏使用的关键 CSS. 也可以使用服务端渲染,加快首屏渲染
  • Pre-cache:预缓存剩余资产 使用第三方代理
  • Lazy load:延迟加载其他路线和非关键资产

针对JS代码执行环境的选择

Web Worker

Web Worker能够让 JavaScript 代码在后台线程上运行, 从而避免阻塞主线程 但是, 并不是所有代码都可移动至Web Work中执行. Web Work 无权访问DOM和许多API(如WebUSB,WebRTC,WebAudio...) 而且, Web Worker 和主线程之间的额外通信开销有时会使事情变慢. 尽管如此, 值得在速度上面做出小的折衷, 换取用户体验的改进

可考虑以下封装好的库: Comlink,Workway,Workerize 不要将 Web Worker 与Service Worker或 Worklet 混淆 大多数模块化工具不支持直接使用Web Work,需要借助插件

Web Worker 与主线程并行运行,但与 OS 线程不同的是,它们不能共享变量。 从历史上看,Web Worker 主要用于将单个繁重的工作从主线程中移出。尝试使用单个 Web Worker 处理多个操作会很快变得笨拙:您不仅必须对消息中的参数进行编码,还必须对消息中的操作进行编码,并且必须进行簿记以匹配请求的响应。

借助Comlink, 使用Web Worker无需考虑postMessage细节

//main.js
const worker = new Worker("./worker.js");
worker.postMessage([40, 2]);
worker.addEventListener("message", event => {
  console.log(event.data);
});
//worker.js
addEventListener("message", event => {
  const [a, b] = event.data;
  // Do stuff with the message
  postMessage(a+b);
});

针对第三方资源的优化

选择满足功能的,打包后代码量最少(支持tree shaking优化...)的第三方库 避免使用功能完全相同的多个库,定时审计清除不需要的第三方库

审计第三方库性能消耗

重点审计CPU占用,网络负载

如果没啥用,就直接移除 如果确实有用,优化加载过程

优化资源加载

  1. 使用async, defer 始终异步加载第三方脚本,除非此脚本需要在页面展示前执行 async: 脚本加载完立即执行,执行顺序和引用顺序没有关系,在load触发前执行,和domContentLoad事件没有必然的先后顺序 defer: 脚本加载完后,在domContentLoad前执行,执行顺序和引用顺序一致
  2. 提前建立连接 重要资源使用preconnect,不那么重要的使用dns-prefetch 浏览器对preconnect和dns-prefetch的支持度不同, 分开使用, 将dns-prefetch作为兜底fallback
<link rel="preconnect" href="http://example.com">
<link rel="dns-prefetch" href="http://example.com">
  1. 懒加载 例如第三方的广告插件,地图插件.....
  • 在主页面加载完后,再加载第三方资源
  • 当用户需要(进入视口)再加载第三方资源.常用的插件库有lazysizes
  1. 优化使用第三方脚本的方式 使用CDN 使用service worker缓存

优化tags

tag是一段代码, 通常用于市场分析和数据分析(发送请求即可,不在意服务器的响应). tag manager可以控制某段代码的执行时机.以google tag manager为例 只触发需要触发的tags(给tags触发设置条件), 可优化性能

不同类型tag的性能比较: 通常image tags(pixels)> custom templates> custom HTML tags 通过tag manager请求的资源通常都是晚于其他资源请求的

通常使用custom template + injectScript API

pixels功能更少,但是更灵活. 由于触发后不涉及JS代码执行, 性能更好,更安全 pixels资源尺寸小(<1KB),不会导致布局偏移 通常使用标签

之前pixels是用于发起服务器响应无关的请求,如数据分析 但是,现在navigator.sendBeacon(), fetch() keepalive更好

tags触发时机

tags执行需要占用一定的资源, 在页面初始阶段(资源紧张)触发tags不是明智的选择 建议在Page Views相关事件(page load, dom ready, window loaded)或自定义事件中触发.

使用自定义事件触发 可以将触发的事件添加到dataLayer中

优化CSS

针对CSS文件 文件本身的优化

压缩css文件

借助打包工具,以webpack为例

  • 提取每个样式表形成单独的文件: mini-css-extract-plugin (+ webpack-fix-style-only-entries (处理webpack4中bug))
  • 压缩CSS: optimize-css-assets-webpack-plugin

延迟加载非关键CSS

CSS 文件是渲染阻塞资源,它们必须在浏览器渲染页面之前加载和处理。 关键样式放在标签中,异步加载其余样式

内联CSS会阻止浏览器缓存 CSS 以便在后续页面加载时重用

<style type="text/css">
.accordion-btn {background-color: #ADD8E6;color: #444;cursor: pointer;padding: 18px;width: 100%;border: none;text-align: left;outline: none;font-size: 15px;transition: 0.4s;}.container {padding: 0 18px;display: none;background-color: white;overflow: hidden;}h1 {word-spacing: 5px;color: blue;font-weight: bold;text-align: center;}
</style>
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>

分离出关键CSS样式和非关键CSS样式

关键CSS 可以理解为 首屏内容用到的CSS

不同设备屏幕尺寸有所不同,首屏内容的像素高度没有统一定义

不同分离CSS工具比较

插件特性使用场景
Critical可提取,压缩和内联首屏CSSGulp,Grunt,webpack
criticalCSS提供对@font-face更精细控制npm模块
Penthouse常用在Angular中,动态注入DOM样式
Critters内联关键 CSS 并对其余部分进行懒加载webpack

针对CSS代码的优化

将定制的样式表和资源(移除用不到的样式或资源)发送给特定的屏幕,减少传输数据量从而提升页面加载性能

响应式背景(使用媒体查询优化CSS背景图像)

body {
  background-position: center center;
  background-attachment: fixed;
  background-repeat: no-repeat; background-size: cover;
}
@media (max-width: 480px) {
    body {
        background-image: url(images/background-mobile.jpg);
    }
}
@media (min-width: 481px) and (max-width: 1024px) {
    body {
        background-image: url(images/background-tablet.jpg);
    }
}

优化WebFonts

浏览器需要先构建依赖DOM和CSSOM的渲染树,才知道需要哪些字体资源来呈现文本 因此,字体资源会在比较晚的时间才开始请求,在获取字体资源前,浏览器可能无法呈现文本

  1. 提前建立连接或是提前下载字体文件
  2. 字体容器格式使用woff2 (最新的字体压缩,浏览器广泛支持)
  3. 使用包含使用文字的最小字体子集,通常是根据页面使用的语言决定

使用pyftsubset工具对字体子集化和优化 有些字体服务可以查询参数手动进行子集化, 详情见字体提供商的文档

@font-face {
    font-family: "Open Sans";
    src: local("Open Sans"),
      url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2");
    unicode-range: U+0025-00FF;
}

local() 使用本地安装的字体 url() 加载外部字体,format()提示所引用的字体格式

  1. 使用系统字体(已下载好)或可变字体(可以有多种变体)作为web fonts的替代

在某些情况下,合成字体是一个可行的方案.但是,为了获得最佳的一致性和视觉效果,请勿依赖字体合成。相反,尽量减少所用字体变体的数量并指定其位置,以便浏览器在页面上使用它们时即会进行下载。或者,选择使用可变字体。

  1. 适当的缓存很有必要 字体资源通常是不经常更新的静态资源.使用浏览器HTTP 缓存机制缓存字体资源 若使用service work, 使用缓存优先策略适用大多数情况

图片资源优化

优先考虑不使用图片. 可使用CSS effect或是web fonts展示对应视觉效果 如果图片非用不可, 考虑优化

位图: 放大不失真.常用于描述简单图形图标,如logo... raster img: 放大失真. 用于描述复杂图片, 如照片...

优化raster image

不同格式raster image对比

FormatTransparencyAnimationBrowser
PNGAll
JPEGAll
WebP现代浏览器
AVIF很少浏览器

查看浏览器是否支持相应的格式:caniuse.com/#feat=webp

  1. 图片格式选择 使用WebP,AVIF作为首选, JPEG,PNG作为备选(fallback)
<picture>
  <source type="image/webp" srcset="flower.webp">
  <source type="image/jpeg" srcset="flower.jpg">
  <img src="flower.jpg" alt="">
</picture>

将图像转换成WebP工具

  • Imagemin WebP插件. npm上有,搭配构建工具很方便
  • cwebp命令行工具.

Imagemin系列有不少插件, 支持对不同图像格式进行有损/无损压缩

  1. 图片压缩 图片压缩很复杂, 深入研究需要阅读大量文献 常见压缩思路:
  • 使用颜色模板, 而不是每个通道使用8bits表示
  • 存储相邻像素之间的差异,而不是存储每个像素单独的值

优化位图

SVG: 二维的, 基于XML的图片格式

  1. 借助SVGO插件, 删除SVG中不必要的meta信息
  2. 传输过程中,确保服务器可压缩SVG文件, 开启GZIP压缩

gif格式图片

将GIF转video, 图片大小可以小很多 FFmpeg可用于将GIF转video

<video autoplay loop muted playsinline>
  <source src="my-animation.webm" type="video/webm">
  <source src="my-animation.mp4" type="video/mp4">
</video>

使用图像CDN

图像 CDN 专门从事图像的转换、优化和分发。 您也可以将它们视为 API,用于访问和处理网站上使用的图像。 对于从图像 CDN 加载的图像,图像 URL 不仅指示要加载的图像,还指示大小、格式和质量等参数。

搭配图像CDN使用URL指示优化选项.png

延迟加载图片

延迟加载: 推迟加载非关键资源. 很有可能会出现在屏幕中的图片不应该使用延迟加载 延迟加载图像和视频减少了初始页面加载时间、初始页面的大小和系统资源使用,所有这些都可以提高性能。 对于图像而言,"非关键"指的是屏幕外

方案1: 浏览器支持

chrome 76+, 支持使用loading属性实现图片的懒加载 对于不支持懒加载的浏览器,可考虑使用插件lazysizes

浏览器支持的延迟加载不适用于 CSS 背景图片, 需要考虑其他方法

<img src="image.png" loading="lazy" alt="…" width="200" height="200">
<script>
  if ('loading' in HTMLImageElement.prototype) {
    const images = document.querySelectorAll('img[loading="lazy"]');
    images.forEach(img => {
      img.src = img.dataset.src;
    });
  } else {
    // Dynamically import the LazySizes library
    const script = document.createElement('script');
    script.src =
      'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.1.2/lazysizes.min.js';
    document.body.appendChild(script);
  }
</script>

loading="eager"并不会提升请求的优先级,fetchpriority="high"可以 loading="lazy" 当用户滚动到懒加载图片附近时,开始加载 附近的距离可视窗口的距离,并不是固定的.取决于多个因素

  • 请求图片资源的类型
  • 有效连接的类型

复杂图片插入页面前先解码,避免短暂的浏览器无响应

并不是在哪里都有效, 会增加懒加载代码的逻辑复杂度 如果是不重要的小图,没有必要这么做

var newImage = new Image();
newImage.src = "my-awesome-image.jpg";

if ("decode" in newImage) {
  // Fancy decoding logic
  newImage.decode().then(function() {
    imageContainer.appendChild(newImage);
  });
} else {
  // Regular image load
  imageContainer.appendChild(newImage);
}

方案2: Intersection Observer

相比于监听事件, Intersection Observer更高效,更易于使用和阅读 兼容所有现代浏览器

<img class="lazy" 
  src="placeholder-image.jpg" 
  data-src="image-to-lazy-load-1x.jpg" 
  data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" 
  alt="I'm an image!">
document.addEventListener("DOMContentLoaded", function() {
  var lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

  if ("IntersectionObserver" in window) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    });

    lazyImages.forEach(function(lazyImage) {
      lazyImageObserver.observe(lazyImage);
    });
  } else {
    // Possibly fall back to event handlers here
  }
});

浏览器支持的延迟加载不适用于 CSS 背景图片, 可以考虑使用Intersection Observer

<div class="lazy-background">
  <h1>Here's a hero heading to get your attention!</h1>
  <p>Here's hero copy to convince you to buy a thing!</p>
  <a href="/buy-a-thing">Buy a thing!</a>
</div>
.lazy-background {
  background-image: url("hero-placeholder.jpg"); /* Placeholder image */
}

.lazy-background.visible {
  background-image: url("hero.jpg"); /* The final image */
}
document.addEventListener("DOMContentLoaded", function() {
  var lazyBackgrounds = [].slice.call(document.querySelectorAll(".lazy-background"));

  if ("IntersectionObserver" in window) {
    let lazyBackgroundObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add("visible");
          lazyBackgroundObserver.unobserve(entry.target);
        }
      });
    });

    lazyBackgrounds.forEach(function(lazyBackground) {
      lazyBackgroundObserver.observe(lazyBackground);
    });
  }
});

方案3: 监听事件scroll,resize,orientationchange

方案4: 使用库

lazysizes: 可以延迟加载图像和 iframe vanilla-lazyload: 可延迟加载图像、背景图像、视频、iframe 和脚本 lozad.js: 仅使用Intersection Observer, 需要polyfill react-lazyload: 没用Intersection Observer,仅适用于React

视频资源优化

延迟加载视频

不同的场景,需要不同的解决方案 也可以使用相应的库: vanilla-lazyload, lozad.js, yall.js , react-lazyload

对于不自动播放的视频

preload="none", 阻止浏览器预加载视频数据

对于代替动画 GIF 的视频

借助Intersection Observer 延迟加载 元素时,需要迭代所有子 元素,并将其 data-src 属性改为 src 属性。完成后,需要通过调用元素的 load 方法来触发视频的加载,此后,媒体将根据 autoplay 属性开始自动播放。

<video class="lazy" autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
  <source data-src="one-does-not-simply.webm" type="video/webm">
  <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>
document.addEventListener("DOMContentLoaded", function() {
  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

  if ("IntersectionObserver" in window) {
    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(video) {
        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });
    });

    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });
  }
});

预加载媒体文件

是否预加载媒体文件, 将其他一些因素加入考量

  • 设备的电池电量 navigator.getBattery().then(battery=>{})
  • “省流模式”用户首选项. 使用Save-Data
  • 网络信息.收费网络预加载前提示用户是否继续
特点
视频预加载属性domContentLoaded之后请求资源,会阻止load事件触发 与MSE(媒体源扩展)不兼容
链接预加载不会阻止load事件触发,domContentLoaded触发前就可以开始请求资源 与MSE和文件段兼容 可以预加载完整视频,也可以预加载第一段
手动缓冲需要web服务器能够支持HTTP Range请求

完整的处理Range请求的方案, 可查看官方示例, 重点注意ranged-response.js github.com/GoogleChrom…

优化网络质量

CDN

CDN根据URL对资源进行缓存, query params,请求头不同, 都需要作为不同资源进行缓存

CDN配置是平衡最小化网络延迟和最大化缓存命中率之间的艺术. 合理利用HTTP缓存,避免不必要的请求,也有助于提升CHR(cache hit ratio)

  • 尽可能多的缓存内容,不同内容采取不同策略. 将经常更新和不常更新的代码拆分到单独的文件中.更新频率不同采用不同缓存策略
  • 避免缓存相同的内容 使用一致的URL 忽略不影响资源的query params, query params排列顺序 在文件名中嵌入文件指纹或是版本号,区分不同内容
  • 通过配置压缩(Brotli),传输层协议(TLS 1.3,HTTP/2. HTTP/3),开启图片压缩可以进一步提升CDN性能

CDN更快的原因

  • 减小RTT(round-trip-time). 离用户更近,RTT更小
  • 使用高度优化的路由转发,而不是基于BGP的路由转发

CDN不能缓存的内容

  • 私人内容不缓存. Cache-Control:private
  • 公共内容可以缓存,但是当请求头带Cache-Control:no-store, 不缓存
  • 动态内容(比如电商网页, API请求)不适合缓存,但是缓存非常短的时间(比如5s),可以减小源服务器负担,同时对内容的及时性影响较小
  • 静态内容(图片,视频,版本库...)尽量缓存.TTL(time to live)很长

CDN缓存策略

添加策略:origin pull. 是个被需要的资源就缓存 移除策略: cache eviction: 容量快不够了移除最近不怎么被访问的资源,或占用大量空间的资源 purging: 调用API移除不需要的缓存

HTTP缓存

HTTP缓存策略不会影响CDN的缓存策略, 但是有利于提高CHR(cache hit ratio) HTTP缓存是避免不必要网络请求的第一道防线. 虽然HTTP缓存不够强大,不够灵活,对缓存响应生命周期控制有限 但是HTTP缓存有一定效率,浏览器都支持,实施的工作量不大

HTTP缓存API集: Cache-Control, ETag, Last-Modified HTTP缓存行为由请求头和响应头一同控制 先查看浏览器缓存,存在满足请求的有效缓存, 读取缓存 缓存过期时,浏览器向服务器发送一个Token(通常是文件内容hash或指纹)用于检查文件是否更改 若服务器返回令牌(ETag),令牌相同说明文件没有改动,不需要重新下载 或者服务器返回资源最新更新时间(Last-Modified),和本地缓存对比判断其是否失效

Cache-Control含义
immutable有效期内不会变更,不需要发送请求确定缓存资源是否有效
no-cache浏览器在使用URL缓存版本前需要向服务器验证
no-store浏览器和其他中间缓存不能存储文件
private浏览器可以缓存文件,中间缓存不行
public浏览器和中间缓存都可以缓存文件
max-age=31536000适用于版本化的资源

Cache-Control值的选择.png

  • 请求头 If-None-Match, If-Modified-Since
  • 响应头(服务端配置) 不配置Cache-Control不会禁用HTTP缓存,浏览器会自行判断 Cache-Control: 指定浏览器和其他中间缓存对单个响应进行缓存的方式和缓存有效时间 ETag: 让验证请求更高效 Last-Modified-Since:让验证请求更高效

加载资源的优先级

HTML,CSS优先级>JS>图片,带async,defer的JS

图片不会阻塞domContentLoaded事件,onload事件需要所有资源都准备好了才会触发 domContentLoaded触发后就可以开始渲染了,不需要等到onload

获取CSS资源用于构建CSSOM, DOM+CSSOM用于构建render tree JS执行前, CSSOM需要构建好

关键渲染路径

关键渲染路径(critical rendering path):首屏绘制需要的所有资源 优化关键渲染路径,浏览器可以更快渲染页面,减少用户看到黑屏的时间 其实就是优化资源的引入方式

提前建立网络连接(使用标签)

资源提示可以以标签的形式写在中, 也可以直接写在HTTP头中 若使用webpack,可用魔法注释, 自动在HTML文件中生成标签 直接写在HTTP头中, 不需要解析标签,更快一点点 若是以匿名模式请求某些资源,比如fonts, 需要添加crossorigin属性

Link: <https://example.com/>; rel=preconnect
<!--as属性可以是script,style,font,image...-->
<link rel="preload" as="style" href="css/style.css">
<link rel="preload" as="script" href="critical.js">

<!--对于以匿名模式加载的字体资源,需要设置crossorigin属性.否者会被加载2次-->
<link rel="preload" href="ComicSans.woff2" as="font" type="font/woff2" crossorigin>
refhint使用场景
dns-prefetch提前进行域名解析重要但又不那么关键的资源,可以先解析域名
preconnect提前建立网络连接用于最重要的连接,其他使用dns-prefetch
prefetch提前下载资源.如果有机会,尽快开始不那么重要资源的请求慎重使用
preload当前页面需要,应尽快开始请求资源不会阻止load事件

资源提示()只是给浏览器提示,不是强制性的指令.具体咋做浏览器视情况而定 连接10s后, 浏览器会自动断开.为避免性能消耗, 需控制连接数在合理的范围

preload

用于浏览器较晚发现的资源 CSS中定义的资源(比如字体文件),CSS文件,JS文件等

preconnect使用场景

  1. 知道地址,但是不知道具体下载地址
db265f32建立连接时不知道, 图片CDN query params建立连接时不知道
https://cdn.example.com/script.db265f32.chunk.js
https://cdn.example.com/snoopy.jpg?key=G3i4emG6B8Jn&size=300x400
  1. 流媒体 先连接连接, 准备好了再处理流

prefetch

可借助插件库,智能地预下载需要资源. 如quicklink,Guess.js

预下载很有可能访问的网页及其相关资源 预下载JS,CSS,图片资源

不会与当前页面需要的资源争带宽资源 预下载好的内容保存在HTTP Cache或memory cache. 用不到的话,过一小段时间就会被删除