1、 背景
上一篇使用GPT调研了,FCP的优化方案
使用chatGPT,设计一套可落地的前端性能优化方案(下)- FCP(First Contenful Paint)的优化方案 - 掘金 (juejin.cn)
本篇呢,继续上一篇的内容,继续调研性能指标的优化方案,定两个主要方向:
- 编译时 —— 构建工具打包时的优化(我主要站webpack队的,以下优化方案,如果没有特别说明,均是webpack方案)
- 运行时 —— 用户从输入URL到展示指标以及对应的优化方案
根据最新的chrome Lighthouse规则,前端的性能指标主要有:
- FCP(First Contenful Paint):首次内容绘制时间,即浏览器首次绘制任何文本、图像、非空白canvas或SVG的时间。
- SI(Speed Index):速度指数,即页面渲染速度的指标。
- LCP(Largest Contentful Paint):最大内容绘制时间,即页面中最大的可见内容元素绘制完成的时间。
- TBT(Total Blocking Time):总阻塞时间,即页面主线程被阻塞的总时间。
- CLS(Cumulative Layout Shift):累计布局偏移,即页面上所有元素在视觉上发生的意外移动的总和。
- TTI(Time to Interactive)TTI是指页面变得可交互所需的时间
- TTD(Time to Display)TTD是指页面显示所需的时间
2、 SI(Speed Index):速度指数,即页面渲染速度的指标
问题一 前端性能指标SI 的优化目标
问题二 如何优化前端性能指标SI
简单总结一下
-
前端性能指标 SI (Speed Index) 是一个衡量页面可视区域渲染速度的指标,它是页面加载过程中每个像素显示的时间的加权平均值,单位是毫秒。SI 越小,表示页面越快呈现给用户,用户体验越好。
-
优化 SI 的目标是让页面的可视区域尽快渲染出来,一般来说,SI 的优化目标是在 3000ms 以内完成。
-
SI的计算方式
//获取si返回时间戳
let navigation = performance.getEntriesByType('navigation')[0].toJSON();
let si = navigation['loadEventEnd'] - navigation['domContentLoadedEventEnd'];
-
优化 SI 的方法有很多,主要包括以下几类:
- 减少 HTTP 请求,合并或删除不必要的资源,使用 HTTP2 协议
- 使用 CDN 加速静态资源的加载,减少网络延迟
- 使用服务端渲染,减少客户端渲染的时间和资源消耗
- 将 CSS 放在文件头部,JavaScript 文件放在底部,避免阻塞渲染
- 使用字体图标代替图片图标,减少图片的大小和请求数
- 压缩文件,使用 Gzip 或 Brotli 等算法压缩 HTML、CSS、JavaScript 等文件
- 善用缓存,不重复加载相同的资源,设置合理的缓存策略
- 使用 Webpack 等工具进行代码分割、懒加载、预加载、图片压缩混淆、去除无用代码、转换 WebP、模块分析、性能分析
- 用 requestAnimationFrame、requestIdleCallback 等 API 优化动画和空闲任务的执行
- 使用 IntersectionObserver API 优化图片懒加载和无限滚动等功能
- 使用 Service Worker API 优化离线访问和推送通知等功能
需要说明一下,SI的优化方式跟FCP的优化,有部分重叠,重复的问题请看上一篇文章哈,本篇着重说一下加粗部分的优化点:
问题汇总:
- 前端缓存有哪些
- 什么是私有缓存
- 什么是共享缓存
- 什么是强缓存
- 什么是协商缓存
- 介绍一下Service Worker 缓存,并提供一个简单的例子
- 介绍一下IndexedDB,并提供一个例子
- 使用 Webpack 等工具进行代码分割、懒加载、预加载、图片压缩混淆、去除无用代码、转换 WebP、模块分析、性能分析
优化点1、善用缓存,不重复加载相同的资源,设置合理的缓存策略
前端性能优化是一个很重要的话题,善用缓存是其中一个有效的方法。缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,可减少等待时间和网络流量,显著提升网站性能。
额外说一下HTTP缓存方案需要在nginx中进行配置哦。常见的缓存有以下几种:
- HTTP-私有缓存:只能用于单独用户,譬如浏览器缓存;
- 私有缓存是通过设置Cache-Control: Private来实现的,这样可以告诉浏览器和其他共享缓存,这个响应只能被单个用户使用,不能被多个用户共享。
-
HTTP-共享缓存:能被多个用户使用,譬如代理服务器缓存;
共享缓存是一种可以被多个用户或进程使用的缓存,它位于客户端和服务器之间,可以减少网络流量和服务器负载,提高网站性能。共享缓存有两种主要的形式:代理缓存和托管缓存。
- 代理缓存是一些代理服务器实现的缓存功能,它们可以根据HTTP头中的指令来判断是否使用缓存。代理缓存通常不由服务开发者管理,而是由网络管理员或用户自己配置。代理缓存有时候可能会导致缓存过期或冲突的问题,因此需要注意设置合适的Cache-Control标头。
- 托管缓存是由服务开发者明确部署的缓存机制,它们可以更灵活地控制缓存的策略和行为。托管缓存的例子包括反向代理、CDN和service worker等。托管缓存通常可以通过自己的配置文件或仪表板来管理缓存,也可以提供一些额外的功能,如删除或更新缓存等。
- HTTP-强缓存:不需要向服务器发送请求,直接从本地获取资源;
强缓存是一种利用HTTP头中的Expires或Cache-Control字段来控制的缓存策略,它可以让浏览器直接从本地缓存中读取资源,而不需要向服务器发送请求。强缓存可以提高网页的加载速度和用户体验。
- Expires和Cache-Control是两种不同的方式来指定资源的缓存时间。Expires是HTTP1.0的规范,它的值是一个绝对的时间点,表示资源在这个时间点之前是有效的。Cache-Control是HTTP1.1的规范,它的值是一个相对的时间段,表示资源在请求发出后多长时间内是有效的。
- 强缓存的优点是可以减少网络流量和服务器压力,提高网页的响应速度。强缓存的缺点是可能导致资源过期或失效,无法及时更新。因此,需要根据资源的变化频率和敏感性来合理设置缓存时间。
- HTTP-协商缓存:需要向服务器发送请求,根据响应头判断是否使用本地资源;
协商缓存是一种利用HTTP头中的ETag或Last-Modified字段来控制的缓存策略,它可以让浏览器在本地缓存过期或不确定时,向服务器发送验证请求,根据服务器的响应来决定是否使用缓存。协商缓存可以保证资源的更新和有效性。
ETag和Last-Modified是两种不同的方式来标识资源的版本。ETag是一个由服务器生成的唯一标识符,表示资源的内容和状态。Last-Modified是一个时间戳,表示资源的最后修改时间。当浏览器向服务器发送验证请求时,会带上If-None-Match或If-Modified-Since字段,分别与服务器端的ETag或Last-Modified进行比较
协商缓存的优点是可以保证资源的更新和有效性,避免缓存过期或失效的问题。协商缓存的缺点是需要向服务器发送验证请求,增加了网络开销和延迟。因此,需要根据资源的变化频率和敏感性来合理设置协商缓存的条件
- Service Worker 缓存:通过编写 Service Worker 脚本来拦截和处理网络请求,实现离线访问和资源预加载等功能;
Service Worker 缓存是利用Cache API来实现的,Cache API是一种提供缓存对象的接口,可以让Service Worker 缓存任意类型的资源,如HTML、CSS、JS、图片等。Cache API可以创建多个缓存对象,并通过名称来区分和管理。Cache API可以对缓存对象进行增删改查等操作。
Service Worker 缓存的基本流程是:
- 注册并安装Service Worker。
- 在安装阶段,打开一个缓存对象,并将需要缓存的资源添加到缓存对象中。
- 在激活阶段,清理旧的缓存对象,并接管页面的控制权。
- 在运行阶段,监听fetch事件,并根据缓存策略来决定是从网络请求资源还是从缓存对象中获取资源。
- 在更新阶段,比较新旧Service Worker 的版本,并替换旧的缓存对象。
首先,我们需要在页面中注册Service Worker,检查浏览器是否支持Service Worker,并指定Service Worker 的文件路径。例如:
// index.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
// 注册成功
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}, function(err) {
// 注册失败
console.log('ServiceWorker registration failed: ', err);
});
});
}
其次,我们需要在Service Worker 文件中定义缓存的名称和资源列表,以及缓存的策略。例如:
// sw.js
// 缓存的名称,可以根据版本号来更新缓存
var CACHE_NAME = 'my-site-cache-v1';
// 需要缓存的资源列表
var urlsToCache = [ '/', '/styles/main.css', '/script/main.js'];
// 安装阶段,打开缓存对象,并将资源添加到缓存中
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
// 激活阶段,清理旧的缓存对象,并接管页面的控制权
self.addEventListener('activate', function(event) {
var cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
}).then(function() {
return self.clients.claim();
})
);
});
// 运行阶段,监听fetch事件,并根据缓存策略来决定是从网络请求资源还是从缓存对象中获取资源
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// 如果命中缓存,就直接返回缓存对象
if (response) {
return response;
}
// 否则,就从网络请求资源,并将响应添加到缓存中
return fetch(event.request).then(function(response) {
// 检查响应是否有效
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应对象,因为它们是流对象,只能消费一次
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
- LocalStorage 和 SessionStorage:通过使用浏览器提供的 Web Storage API 来存储键值对数据,实现持久化和会话级别的缓存
这个就不多说了吧,别说不会哈~
- IndexedDB:通过使用浏览器提供的 IndexedDB API 来存储结构化数据,实现大量数据和复杂数据类型的缓存。
IndexedDB是一种用于客户端存储大量结构化数据的低级API,包括文件和二进制数据。这个API使用索引来实现对这些数据的高性能搜索。虽然Web Storage对于存储较小的数据量很有用,但对于存储较大的结构化数据则不太有用。IndexedDB提供了一个解决方案。
IndexedDB是一个事务型数据库系统,类似于基于SQL的RDBMS。然而,不像RDBMS使用固定列表,IndexedDB是一个基于JavaScript的面向对象数据库。IndexedDB允许你存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。你需要指定数据库的模式,打开一个连接到你的数据库,然后在一系列事务中检索和更新数据。
IndexedDB提供了同步和异步两种操作方式。同步操作只能在Web Workers中使用,但目前没有浏览器实现了这种方式。异步操作可以在Web Workers或普通页面中使用,但Firefox目前还不支持这种方式。
IndexedDB的优点是可以存储大量的结构化数据,并通过索引进行快速查询和排序。IndexedDB的缺点是需要使用复杂的API,兼容性不太好,存储容量有限。
具体代码,大家自行百度一下吧,有点多,太占篇幅了哈
-
使用 Webpack 等工具进行代码分割、懒加载、预加载、图片压缩混淆、去除无用代码、转换 WebP、模块分析、性能分析
-
代码分割是 webpack 中最引人注目的特性之一。 此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。 代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
常用的代码分离方法有三种:
- 入口起点 :使用 entry 配置手动地分离代码,如react项目中将react依赖分离出去。
- 防止重复 :使用 Entry dependencies 或者 SplitChunksPlugin 去重和分离 chunk。
- 动态导入 :动态导入是 JavaScript ES2020 规范中引入的功能之一。 这个特性使得 ES2015(ES6)中引入的模块更加有用和强大。
- 懒加载或者按需加载,是一种很好的优化网页或应用的方式。 这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。 这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
举个例子,比如页面中有个协议弹窗,里面是大量的静态文案,对于这样的模块,我们可以使用webpack的import(),让用户点击的时候在加载协议内容。
- 预加载是指在主 bundle 中请求文件,但是会告诉浏览器该文件会在未来的某个时刻被用到。 预加载可以使用 webpack 的内置支持来实现。
优化点2、1. 使用 Webpack 等工具进行代码分割、懒加载、预加载、图片压缩混淆、去除无用代码、转换 WebP、模块分析、性能分析
在 webpack 中应用预加载非常简单,只需要在 dynamic import 中添加相应注释,webpack 就会知道你需要对这个 chunk 进行预加载。
// prefetch
//这是一个使用 webpack 的魔法注释(magic comments)来实现预拉取(prefetch)的语句。
//预拉取是指在浏览器闲置时,提前下载可能在未来需要的资源。这样可以提高应用的性能和用户体验。
//这个语句的意思是,当主模块加载完成后,浏览器会在空闲时间下载 sub1.js 文件,并缓存起来,等到真正
//需要使用时,就可以直接从缓存中获取,而不用再发起网络请求。
import(/* webpackPrefetch: true */ './sub1.js');
// preload
//这是一个使用 webpack 的魔法注释(magic comments)来实现预加载(preload)的语句。
//预加载是指在主模块加载时,以并行方式开始加载可能在当前导航下需要的资源。这样可以提高应用的性能
//和用户体验。
//这个语句的意思是,当主模块加载时,浏览器会同时下载 sub2.js 文件,并缓存起来,等到真正需要使用
//时,就可以直接从缓存中获取,而不用再发起网络请求。
import(/* webpackPreload: true */ './sub2.js')
-
图片压缩混淆、去除无用代码、转换 WebP 等操作可以通过 webpack 的 loader 和 plugin 来实现,例如 image-webpack-loader, terser-webpack-plugin, webp-loader 等。
-
模块分析和性能分析可以通过 webpack 的内置支持或者第三方工具来实现,例如 webpack-bundle-analyzer, speed-measure-webpack-plugin, webpack-dashboard 等。
优化点3、 使用 IntersectionObserver API 优化图片懒加载和无限滚动等功能
IntersectionObserver API 是一种异步检测目标元素与祖先元素或视口相交情况的变化的方法。这个API允许我们实现诸如无限滚动和图像懒加载等酷炫的功能。
要使用这个API,你需要创建一个 IntersectionObserver 对象,并传入一个回调函数和一个配置对象。回调函数会在目标元素与根元素相交时被触发,配置对象可以指定一个自定义的根元素或使用默认值。你还可以设置一个 rootMargin 参数来扩展或缩小监视区域。
你可以使用 observe 方法来监视一个或多个目标元素,使用 unobserve 方法来取消监视,使用 disconnect 方法来关闭观察器。
在 React 中,你可以使用 useEffect 钩子来进行 API 调用,使用 useReducer 钩子来管理状态。你也可以创建自定义钩子来封装 IntersectionObserver 的逻辑
// 创建一个 IntersectionObserver 对象
let observer = new IntersectionObserver((entries) => {
// 遍历每个被观察的目标元素
entries.forEach((entry) => {
// 如果目标元素与视口相交
if (entry.isIntersecting) {
// 获取目标元素的 img 子元素
let img = entry.target.querySelector("img");
// 将 img 的 data-src 属性值赋给 src 属性,从而触发图片加载
img.src = img.dataset.src;
// 取消对目标元素的观察,避免重复加载
observer.unobserve(entry.target);
}
});
});
// 获取所有要懒加载的图片元素的父元素
let boxes = document.querySelectorAll(".box");
// 对每个父元素进行观察
boxes.forEach((box) => {
observer.observe(box);
});
3、小结
好的针对SI的优化,应该就这些了,有一大部分的优化跟FCP是重复的,我们在重复一下目标。
SI (Speed Index)原则上越小越好,最大不可超过3000ms。SI 越小,表示页面越快呈现给用户,用户体验越好。
大多数情况下,我们用webpack做好优化,把缓存配置好,基本就能够达到了,如果需要额外的优化,在尝试文中的其他方案吧。
由于篇幅所限,其他的性能指标放在下一篇来分享
本文正在参加 人工智能创作者扶持计划