29 经验:优化就是减少请求数量和质量
你好,我是三桥。
前几节课,我们主要围绕Web Vitals的核心指标优化了前端性能问题。
前端全链路的性能优化是一个复杂的问题。一些常见的优化技巧,总结起来大概有6点。
- 优化以减少请求量,包括合并资源、减少HTTP请求数、
gzip
压缩、懒加载等。 - 优化以加快请求速度,包括DNS预解析、减少域名数量、HTTP2协议、CDN分发、WebP图片格式等。
- 缓存优化,包括离线缓存
manifest
、HTTP协议的缓存请求等。 - 渲染优化,包括服务器端渲染(SSR)、客户端渲染(CSR)、静态站点生成(SSG)等。
- CSS优化,具体方法包括避免使用
table
布局、减少深层级的选择器、使用className
批量修改元素样式、利用GPU渲染动画等。 - 前端工程化,例如代码分包、
tree shaking
等。
这些优化方法都是前端技术的基础,但在实际性能优化的时候不一定都会用到。这节课,我们就梳理一些容易被忽视但非常实用的技巧。有4个方向,用户网络环境、小图标、响应式设计以及缓存数据优先。
用户网络环境
我们的目标是让前端页面快速触达用户,让用户优先体验到内容,然后才是功能交互。
第27节课,我们在4G网络和不缓存资源两个条件下模拟了低速网络用户的情况,并对此进行了性能优化。
对于低速网络的用户,我们需要提供快速访问网站的条件,最直接的解决方式是根据用户的网络状态调整我们提供的资源。
目前,前端页面可以通过使用 Network Information API
来判断网络状态。下面是使用这个API的基本示例。
let type = navigator.connection.effectiveType;
function onConnectionChange() {
console.log(
`设备的网络连接从 ${type} 变为了 ${navigator.connection.effectiveType}`,
);
type = navigator.connection.effectiveType;
}
connection.addEventListener("change", onConnectionChange);
代码中的第一行使用了 navigator.connection.effectiveType
返回连接的有效类型。这是一个衡量往返时间和下行链路的标准,也能用来判断用户的网络速度。然后,我们可以通过 connection
对象的change事件来监听网络的变化。
除此之外,connection
对象还提供了其它四个属性来判断用户网络状况,包括downlink
、rtt
、saveData
和 type
。其中,downlink
和 rtt
主要是用于测量速度和网络稳定性的。
在上述代码之上,我们还可以调整代码,继续完善。比如使用netSpeed全局变量来判断用户当前的网络状态,具体代码如下。
function checkEffectiveType() {
if(/\slow-2g|2g|3g/.test(navigator.connection.effectiveType)){
return false;
}
return true
}
// 定义全局变量,告诉程序当前用户网络质量
// true为高速网络,false为低速网络
let netSpeed = checkEffectiveType();
// 前端如何判断用户的网络质量,浏览器原生的API:navigator.connection
function onConnectionChange(){
netSpeed = checkEffectiveType()
}
navigator.connection.addEventListener('change', onConnectionChange);
都是判断网络状态,有了netSpeed全局变量后又有什么不一样吗?有的,我们可以通过4个方法来提升用户体验。
- 根据网络速度的快慢,提供不同格式的资源,例如高清或标清图片。
- 当用户网速较慢时,可以选择默认字体,不用网络字体。
- 通过监控,帮助用户解决网络质量问题,如提醒用户更换网络,或注意流量费用。
- 网速较慢时,增加延迟上传或下载的提醒。
比如第一点,我们可以利用网络速度状态,区分用户,并提供不同的图片,例如大小、质量等。参考代码如下。
// 定义全局变量,告诉程序当前用户网络质量
// true为高速网络,false为低速网络
let netSpeed = checkEffectiveType();
// 阿里云OSS图片参数
let ossParams = 'x-oss-process=image/resize,w_200/quality,q_80'
const exampleImage = 'https://oss-console-img-demo-cn-hangzhou.oss-cn-hangzhou.aliyuncs.com/example.jpg'
function checkEffectiveType() {
if(/\slow-2g|2g|3g/.test(navigator.connection.effectiveType)){
ossParams = 'x-oss-process=image/resize,w_100/quality,q_60'
return false;
}
return true
}
// 前端如何判断用户的网络质量,浏览器原生的API:navigator.connection
function onConnectionChange(){
netSpeed = checkEffectiveType()
if (!netSpeed) {
ossParams = 'x-oss-process=image/resize,w_100/quality,q_60'
} else {
ossParams = 'x-oss-process=image/resize,w_200/quality,q_80'
}
}
navigator.connection.addEventListener('change', onConnectionChange);
// 完整OSS图片地址
console.log(exampleImage + '?' + ossParams)
在这个例子中,我们通过定义全局变量ossParams来记录图片需要使用哪种参数。如果用户处于高速网络状态,那就返回200px宽度和80%相对质量的图片;否则,返回100px宽度和60%相对质量的图片。
小图标
我们讨论了很多优化图片加载的方法,但你有没有发现,我说的都是大面积的图片,那那些小的不起眼的图片怎么办呢?
小图片,通常是指小于5K,或者宽度和高度小于50像素的图片。网页图片的加载是异步的,小图片过多会影响用户首次访问的加载速度,尤其是Icon图片。
我们来看看H5站点使用Icon图片的地方。
上图展示了两个H5站,我列出了五个使用 Icon
小图标的地方。如果你是前端新手,可能会直接用图片替代这些小图标,但这并不是最佳做法。最佳的做法是使用SVG来实现Icon小图标。
我给你提供三种方法来获取 Icon
图标SVG代码。
首先,大多数前端项目都是基于Vue和React的后台管理系统,通常会选择 Element
或 Ant Design
作为主要的前端UI框架。对于这类项目,UI框架通常都提供了基于SVG的 Icon
图标组件,前端同学来可以直接使用,因此这里不再详细说明。
第二,如果研发团队有设计师,通常UI设计稿会有大量的小图标。设计师通常会直接提供图片素材,包括Icon图标。
在这种情况下,我们可以使用如蓝湖这类的设计平台,将 Icon
图标导出为SVG代码。以下图示只需三步即可完成SVG代码的下载。
然后,我们只需要将SVG代码封装成组件,便可以直接引用,参考代码如下。
import React from "react";
export const ArrowIcon = () => (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Group 337">
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M9.84853 2.35148C9.3799 1.88285 8.6201 1.88285 8.15147 2.35148C7.68284 2.82011 7.68284 3.57991 8.15147 4.04854L9.80294 5.70001L1.2 5.70001C0.537258 5.70001 0 6.23727 0 6.90001C0 7.56275 0.537258 8.10001 1.2 8.10001L9.40294 8.10001L8.15147 9.35148C7.68284 9.82011 7.68284 10.5799 8.15147 11.0485C8.6201 11.5172 9.3799 11.5172 9.84853 11.0485L13.3485 7.54854C13.8172 7.07991 13.8172 6.32011 13.3485 5.85148L9.84853 2.35148Z" fill="#00CA51"/>
</g>
</svg>
)
第三种方法适用于没有设计师的前端团队。前端同学可以根据项目需求,在一些图标平台上找到合适的图标,并将其转换为SVG代码。
比如,我经常使用的是iconfont.cn。通过下面的示图中的四个步骤,你就可以得到图标的SVG代码。
总的来说,只要网页中的图片可以用SVG来表示,那就尽量使用SVG格式。这样能够减少网页加载过程中图片的异步加载数量,提高网页加载速度。
响应式设计
响应式设计的概念已经存在一段时间了。在早期,我们会根据用户设备的屏幕尺寸调整页面布局。但现在,由于设备类型和尺寸越来越多,我们已经很少使用响应式设计来实现网页布局。
不过,可能你没有意识到,响应式设计还可以帮助我们解决性能问题。其中,CSS媒体查询是响应式设计的关键部分,它能让我们根据视口的大小创建不同的布局。
例如,当媒体查询发现视口宽度小于480像素时,我们可以初步判断用户设备屏幕尺寸较小。再结合前面的 navigator.connection
对象,我们可以判断用户是否在低速网络下使用低端设备访问前端页面。
以下示例代码是通过媒体查询,区分三种不同屏幕尺寸的CSS样式。
// media.css
//屏幕分辨率介于360px与600px之间时不使用背景图
@media screen and (min-width: 360px) and (max-width: 600px) {
body {
background: none;
}
}
//屏幕分辨率介于600px与1024px之间时使用普通背景图
@media screen and (min-width: 600px) and (max-width: 1024px) {
body {
background:no-repeat url("./bg.png");;
}
}
//屏幕分辨率大于1024px时使用高清背景图
@media screen and (min-width: 1024px) {
body {
background:no-repeat url("./bg-retina.png");;
}
}
代码逻辑简单明了,如果用户的屏幕较小,我们就不在CSS中使用图片作为背景。如果屏幕大于1024像素,我们就认为他的设备硬件较好,并且可能在高速网络中,所以我们可以提供更高清晰度的图片作为背景。
然而,适配不同屏幕分辨率的代码会随着页面布局的增多而增加。为了减少媒体查询代码的增多,我们可以利用媒体查询的另一个特性来分解代码。
我们知道,浏览器允许在 link
元素上添加 media
属性。所以,根据屏幕分辨率大小,我们可以选择有效的CSS内容并进行解析。例如,我们可以将上述代码改造成三个CSS样式文件。
<link rel="stylesheet" href="css/mobile.css" media="screen and (min-width: 360px) and (max-width: 600px)">
<link rel="stylesheet" href="css/tablet.css" media="screen and (min-width: 600px) and (max-width: 1024px)">
<link rel="stylesheet" href="css/desktop.css" media="screen and (min-width: 1024px)">
不过,使用媒体查询的优化方案不适用于整个网页的布局适应,你可以把它看成一个降级方案,减少不必要的资源请求。这样,低速网络或低端设备的用户可以快速访问页面,从而提高页面转化率。
缓存数据优先
我在第23节课讲解了三星合作项目里的H5信息流项目。我们团队针对信息流数据做了一个小优化,就是缓存文章列表数据。
为什么我们要进行这样的数据缓存呢?
我们知道,前端请求业务数据是异步的,如文章列表、文章推荐列表、购物车商品等。对用户来说,这种异步请求获取数据存在延迟。如果接口请求时间过长,页面内容出现的时间也会延长,甚至会出现部分白屏。
为了解决数据获取延迟导致的页面加载慢的问题,前端可以采用缓存数据优先的策略,保证页面的完整性。
因此,无论信息流列表是新的还是旧的,当用户打开三星浏览器时,都能够立即显示。在有内容的情况下,用户无需等待就可以浏览历史数据,就像今日头条的首页一样,每次打开都默认有信息流文章。
我们看看在三星合作项目中的数据流程图。
缓存优先的策略是,前端页面首先读取并显示本地缓存的数据,同时异步请求新数据。新数据返回后,更新缓存并显示最新数据。
这种策略实现逻辑简单,只需使用LocalStorage就能实现数据缓存。下面是一个基于这种策略的代码示例。
// 从本地缓存中读取数据
let cacheData = localStorage.getItem('cacheData');
if (cacheData) {
// 如果缓存数据存在,先渲染缓存数据
try {
render(JSON.parse(cacheData));
} catch (err) {
console.error(err);
}
}
// 发起异步请求获取最新数据
fetch('<https://api.example.com/data>')
.then(response => response.json())
.then(data => {
// 更新缓存数据
localStorage.setItem('cacheData', JSON.stringify(data));
// 渲染最新数据
render(data);
});
这段代码首先试图从 LocalStorage
中获取缓存数据。如果存在,就先用这些数据渲染页面。无论是否存在缓存数据,都会异步获取最新数据。当最新数据返回时,更新 LocalStorage
中的缓存,并用这些数据重新渲染页面。
不过,缓存优先策略存在一定的限制,并非适合大多数场景,因为只有在相同浏览器下用户才重复使用缓存数据,例如在同一个App的WebView当中。而且,LocalStorage
的存储空间有限,如果需要缓存的数据过多,可能会导致存储空间不足。此外,由于缓存数据可能会过期,我们也要定期更新或清除缓存数据,确保用户始终获得最新数据。
总的来说,本地缓存数据优先渲染页面,可以减少白屏时间,改善用户体验。
总结
总结一下,本节课我们学习了四种前端性能优化小技巧。
采用SVG小图标已成为Web开发的标准做法。网络环境判断和响应式设计都是为了适配用户群体,确保快速触达用户。缓存优先策略在特定场景下先显示内容,再延后更新新内容,让用户先快速体验页面功能。
虽然每种优化技巧有其适用场景,但在实践中,我们不能忘记最基本和最常见的优化方法。这些基本方法为提高前端页面的性能和效率提供了坚实的基础。
到目前为止,前端性能优化没有固定标准和方法,只有实践经验。实际上,前端性能优化就是在做减法,减少不必要的请求和文件大小,优化异步链路,根据用户群体做降级方案,其最终目的是提升业务转化率并促成交易。
同样,前端全链路性能优化以用户为中心,不仅要解决用户问题,还要提升产品用户体验。下节课我们一起来学习如何使用工具来辅助前端全链路优化。
思考题
在本节课的最后,给你布置一道思考题。
前端全链路性能优化和常见性能优化的基本同理相似。在你参与的性能优化当中,还有哪些是减少请求量和优化链路的经验?我们可以一起探讨一下。
欢迎你在留言区和我交流。如果觉得有所收获,也可以把课程分享给更多的朋友一起学习。我们下节课见!
- 苏果果 👍(0) 💬(0)
完整源码入口: https://github.com/sankyutang/fontend-trace-geekbang-course
2024-07-18