Web 传输的内容当然是越少越好,最近一段时间的工作一直致力于 Web 性能优化,这是我近期使用过的一些缩减 Web 体积的手段
这些手段主要是为了减少 Web 传输的内容大小,只有干货
CSS
🐛删除无用的样式
在使用 UI 库的时候,UI 库提供的样式并不是所有的都会使用到
例如一个 button 组件一般都会提供 default/primary/success/warning/danger 五颜六色好几款样式
但我们实际一个项目中也许只会用到其中的一两种,为了减少样式表的体积,需要将那些没有使用的样式挑选出来删除掉
使用 uncss 工具来删除无用的样式
该工具提供有在线版,只需要复制自己的 HTML 以及 CSS,点击按钮就可以生成精简后的样式
另外也可以通过浏览器工具 Coverage 挑选出未使用的样式,如下图
经过分析得出每个文件未使用样式的百分占比,其中红色标记的为未使用到的样式,从下图中可以看到具体未使用到的样式有哪些
ℹ️ 许多框架和库也提供自定义打包版本,从源头舍去那些无用的代码
🐛删除重复的样式
CSS 全名 层叠样式表(Cascading Style Sheets),对同一个元素多次指定同一个样式只会让优先级高的覆盖优先级低的
在样式规则的选择器完全相同的情况下(比如这里 .selector-1 > .selector-2 和 .selector-1 > .selector-2 是完全相同的),被覆盖的样式可以安全地删除,如下
/* Before */
.selector-1 > .selector-2 {
display: none;
width: 200px !important;
}
.selector-1 > .selector-2 {
display: block;
width: 100px;
}
/* After */
.selector-1 > .selector-2 {
width: 200px !important;
}
.selector-1 > .selector-2 {
display: block;
}
通过浏览器的开发者工具可以轻松看到哪些样式被覆盖了
⚠️ 在选择器不相同的时候,也有可能会匹配到同一个元素,这个时候本条规则并不适用,需要注意
⚠️ 有时候同一个样式属性反复出现只是为了兼容一些旧浏览器,也需要注意
🐛使用复合属性
有些样式属性可以合并为一条,比如
/* Before */
.selector {
flex-direction: column;
flex-wrap: wrap;
}
/* After */
.selector {
flex-flow: column wrap;
}
合并之后字节数减少
🐛删除过时的样式
有些样式是为了兼容一些老旧浏览器而提供的,当前已经不需要再兼容这些浏览器了,对应的样式可以删除掉,比如如下这些
- header {
- display: block;
- }
ℹ️ 使用 autoprefixer 删除过时的浏览器厂商前缀(比如 -moz-,-ms- 这些)
🐛利用继承
部分样式会继承给后代元素,后代元素没有必要再写一遍,除非是确实需要覆盖的
之所以会有这条是因为之前在项目中看到随处可见的 box-sizing: border-box
属性其实可以主动设置为继承
*,
*:before,
*:after {
box-sizing: inherit;
}
html {
box-sizing: border-box;
}
这样所有元素都会继承这个属性,不用反复定义
🐛提取公共样式
将多个规则集中相同的样式提取出来,并使用群组选择器放在一起,比如
/* Before */
.badge {
background-color: orange;
border-raidus: 5px;
color: #fff;
font-size: 13px;
}
.label {
background-color: orange;
border-raidus: 5px;
color: #fff;
font-size: 12px;
}
/* After */
.badge,
.label {
background-color: orange;
border-raidus: 5px;
color: #fff;
}
.badge {
font-size: 13px;
}
.label {
font-size: 12px;
}
csscss 可以用来分析冗余的 CSS 代码
这是一个 Ruby 工具,使用前需要先安装 ruby1.9 或以上版本
这个工具只是用来分析冗余样式的,并不会主动删除样式,需要自己手动调整
⚠️ 在 CSS 中,样式的先后顺序是有意义的,随意移动样式规则可能会让样式出现问题,需要经过严格测试
ℹ️ csso 可以用来删除冗余,合并样式规则
🐛压缩 CSS
压缩主要是删除无用的空白和注释,或用更简短的写法代替
推荐使用工具 cssnano 来压缩 CSS
该工具还提供了 在线版
这是压缩前的代码
这是压缩后
@charset "utf-8";h1:before{margin:10px 20px;color:red;font-weight:400;background-position:100% 100%;quotes:"«" "»";background:linear-gradient(180deg,#ffe500,#ffe500 50%,#121 0,#121);min-width:0}
体积减少了一半
ℹ️ cssnano 自带 autoprefixer 工具帮助清理浏览器厂商前缀
JavaScript
🐛删除无用的 JavaScript
浏览器的 Coverage 工具也能挑选出未使用的 JavaScript 代码,不再重复
⚠️ 同样的,挑选出来的代码也不一定全是无用的,需要经过仔细测试
🐛删除历史遗留代码
同 CSS 一样,JavaScript 也有一些代码是为了兼容旧浏览器而存在的
像 es5-shim.js 就是为了给那些不支持 ES5 的浏览器准备的,现在已经可以放心地从项目中去掉了,目前全球使用支持 ES5 的浏览器的用户占比高达98%
另外一些框架或库的新版本通常将不会包含那些兼容旧浏览器的代码,需要时保持更新即可,比如用 jQuery3.0 替换 jQuery1.12
🐛删除功能重复的插件
一个项目经手的人多了之后,会出现一些匪夷所思的膨胀,比如同一个项目中引入了好几个功能相似的插件
找出相关代码,根据需求确定真正需要使用的插件,去掉其它多余的
⚠️ 此条需要经过严格的测试
🐛用 CSS 代替 JavaScript 实现效果
常见的比如鼠标移入区域的时候显示元素,移出的时候隐藏元素,用 CSS 可以轻易实现
.selector + .item {
display: none;
}
.selector:hover + .item {
display: block;
}
You-Dont-Need-JavaScript 这个仓库展示了很多可以不依赖 JavaScript 实现的效果
🐛使用新的 API
随着 Web 标准的丰富以及浏览器的更新换代,越来越多的功能可以通过设备/浏览器原生的 API 来实现
比如 IntersectionObserver 可以用来探测 DOM 元素是否位于窗口可视区域内,这就不需要借助插件来实现这些功能了
相应的插件代码可以从项目中安全地删除,或者只为那些老旧设备/浏览器提供
🐛压缩 JavaScript
主要是删除没用的空白和注释等等
使用 Terser 来压缩 JavaScript,通过 NPM 安装 npm install terser -g
执行命令 terser main.js -o main.min.js -c -m
字体
🐛选择合适的格式
常用的字体格式有如下这些
WOFF2/WOFF
Web 开放字体格式(Web Open Font Format),加载快,压缩率高
WOFF2 是 WOFF 的升级版本,压缩率更高
SVG/SVGZ
矢量图形字体(Scalable Vector Graphics Font),仅有少部分浏览器支持(比如 iOS Safari 4.1-)
EOT
Embedded Open Type,IE 独占
TTF/OTF
OpenType Font 和 TrueType Font,浏览器支持范围最广的格式
根据目标设备选择合适的字体格式,不同的字体格式兼容的浏览器也是不一样的
下图是图一套字体的不同文件格式的大小对比
我们应该优先选用压缩率更高的 WOFF2 文件格式,如果浏览器不支持该格式,降级到 WOFF,甚至 OTF/TTF
下面是完整定义字体的方式,浏览器会根据优先顺序下载自身能识别但体积相对更小的字体文件
@font-face {
font-family: 'My Font';
src: url('path/my-font.eot');
src: url('path/my-font.eot?#iefix') format('embedded-opentype'),
url('path/my-font.woff2') format('woff2'),
url('path/my-font.woff') format('woff'),
url('path/my-font.ttf') format('truetype'),
url('path/my-font.svg#svgFontName') format('svg');
}
- TTF/OTF 的兼容性仅比 WOFF 多出一点点而已,已经到了可以忽略不计的地步
- SVG 字体和 EOT 是针对部分旧版本浏览器的兼容方案,目前已经没有太大使用的价值
所以上面的字体定义也可以精简为如下,基本可以满足市面上的主流浏览器
@font-face {
font-family: 'My Font';
src: url('path/my-font.woff2') format('woff2'),
url('path/my-font.woff') format('woff');
}
🐛剔除多余的字体
在一个字体文件中不是所有字体都会使用到,特别是在使用图标字体的时候
里面有很多图标是我在项目中没有用到的,这种时候就需要编辑字体文件,删除那些没用上的字体
百度有个在线字体编辑工具 http://fontstore.baidu.com/static/editor/index.html 可以打开并编辑字体以及保存为其它格式
这是经过我编辑过后的文件大小对比,文件大小差距很大,确实用到的字体比较少
图像
在 Web 网页中,图像的体积占了大头,减少图像可以大幅增加性能
🐛选择适合的图像格式
不同文件格式的图像其文件大小,图像质量是不一样的,根据具体情况选择合适的图像格式
常用 Web 图像格式
一些新的图像格式拥有较高的性能,比如 AVIF 和 WebP
不过这些新的图像格式不是所有浏览器都支持,此时可以使用一个 <picture>
元素来包裹 <img>
元素,再通过使用 <source>
元素来为 <img>
元素提供多个备胎资源供其自行选择
<source>
元素可以有多个,srcset 属性是必须的(注意是 srcset)
<picture>
<source type="image/avif" srcset="logo.avif">
<source type="image/webp" srcset="logo.webp">
<img alt="logo" src="logo.png">
</picture>
浏览器会自行忽略不支持的格式,如果浏览器支持 AVIF 格式就使用 logo.avif,如果支持 WebP 格式就使用 logo.webp
如果上面俩都不支持,就会使用 logo.png,不支持 <picture>
元素的浏览器会直接显示 <img>
元素
值得一提的是 <picture>
元素内部必须包含一个 <img>
元素,否则图像不会显示(因为 <picture>
元素并不是一个独立显示的元素,而是为 <img>
元素服务的)
还有 <img>
元素始终都不应该忘记的 alt 属性,当任何图像格式都无法显示或者图像下载失败的时候,至少还能显示替代的文字说明
要在 CSS 中使用这些新的格式通常用 JavaScript 来判断浏览器是否支持
创建一个 Image 对象,然后加载一张较小的需要判断格式的图像,如果加载成功则说明浏览器支持该格式,下面是 Google 提供的判断浏览器是否支持 WebP 的方法
const img = new Image()
img.onload = img.onerror = () => {
document.body.classList.add(img.height > 0 ? 'webp' : 'no-webp')
}
img.src = 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=='
如果该浏览器支持,则给 <body>
元素添加 webp
类,否则添加 no-webp
,在 CSS 中就可以这样写
.webp .logo {
background: url(./logo.webp);
}
.no-webp .logo {
background: url(./logo.png);
}
这样就能根据该浏览器是否支持 webp 格式加载不同格式的图像了
ℹ️ 转换图像格式可以使用 Sqoosh,在图像大小和质量之间手动调整权衡
🐛使用响应式图像
在 CSS 中使用媒体查询结合 image-set 可以依据设备/浏览器的宽度以及像素比显示不同分辨率的图像
ℹ️ 为了方便一眼看出来,图像的名称包含了图像的真实宽度,比如 logo-240.png 表示这张图像宽度为 240 像素
.logo {
background-image: url(./images/logo-120.png);
background-image: -webkit-image-set(url(./images/logo-120.png) 1x,
url(./images/logo-240.png) 2x);
background-image: image-set(url(./images/logo-120.png) 1x,
url(./images/logo-240.png) 2x);
}
@media (min-width: 600px) {
.logo {
background-image: url(./images/logo-240.png);
background-image: -webkit-image-set(url(./images/logo-240.png) 1x,
url(./images/logo-480.png) 2x);
background-image: image-set(url(./images/logo-240.png) 1x,
url(./images/logo-480.png) 2x);
}
}
@media (min-width: 1200px) {
.logo {
background-image: url(./images/logo-480.png);
background-image: -webkit-image-set(url(./images/logo-480.png) 1x,
url(./images/logo-960.png) 2x);
background-image: image-set(url(./images/logo-480.png) 1x,
url(./images/logo-960.png) 2x);
}
}
根据 移动优先 的原则,使用媒体查询应该从小往大
ℹ️ 不支持 image-set
的浏览器将会使用前面定义的传统 url 路径
⚠️ image-set
目前还在草案中,需要添加对应的浏览器厂商前缀,示例已添加
⚠️ Safari 只支持 url 路径和 1x/2x 这样的设备像素比
而 <img>
元素通过其新增的 srcset 和 sizes 属性来实现响应式图像
<img alt="avator"
src="avator-120.jpg"
srcset="avator-120.jpg 120w, avator-240.jpg 240w, avator-480.jpg 480w"
sizes="(max-width: 600px) 120px, 240px">
srcset 属性为图像提供多个源供设备/浏览器自行选择,其中图像路径后面的 120w/240w/480w 用于告诉设备/浏览器每张图像的实际宽度
sizes 属性为图像提供渲染尺寸,可以通过媒体查询提供多个渲染尺寸以及一个默认尺寸(这里 240px 就是默认的渲染尺寸)
设备/浏览器会根据这些信息选择最合适的图像加载显示
当设备/浏览器宽度在 600 像素以下时图像将占据 120 像素的宽度,此时如果设备像素比为 1 则显示 avator-120.jpg,如果设备像素比为 2 则显示 avator-240.jpg,为 4 则应该显示 avator-480.jpg
当设备/浏览器宽度大于 600 像素的时候图像将占据 240 像素的宽度,此时如果设备像素比为 1 则显示 avator-240.jpg,如果设备像素比为 2 则显示 avator-480.jpg
ℹ️ 设备像素比也有可能是小数,比如 1.5,设备/浏览器会选择它自己认为最合适的那张图像来显示
ℹ️ 其中 src 属性是给不支持 srcset 和 sizes 属性的浏览器提供的
🐛图像压缩
有些格式的图像往往还会包含一些没有用的信息,清理掉这些信息有助于缩小图像体积
这通常使用工具来进行
使用 imagemin 压缩图像
🐛图像懒加载
页面上有很多图像我们一开始是看不到的,有的在我们滚动页面之后才会出现在屏幕上,又有的在某个对话框弹出后才能看到
对于这类图像,我们可以推迟它们的加载时机,等到它们需要真正展示在屏幕上的时候才加载,而不是在页面一开始时就加载,这将大大节省页面初始化时加载的资源大小
使用浏览器原生的懒加载方案,这非常简单,只需要给图像元素添加一个 loading="lazy"
属性即可
<img alt="avator" loading="lazy" src="avator.jpg">
目前该属性只得到一部分浏览器的支持,不支持的浏览器会忽略
该属性的 polyfill
还可以使用 JavaScript 插件,市面上有不少这类型的插件
⚠️ 引入一个插件会增加 JavaScript 的代码量,但是延迟了部分图像的加载时机,具体需要权衡
🐛使用其它方案替换图像
减少图像最好的办法就是没有图像
使用 SVG 替换图像
上面这张图像格式为 png 大小为 1.46kb
下面是使用 SVG 来表示同样的图像的代码,只有 300 多字节,体积大幅度减小
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="M50 92.5H6.09a4.47 4.47 0 01-3.87-6.71l22-38 22-38a4.46 4.46 0 017.74 0l22 38 22 38a4.47 4.47 0 01-3.87 6.71z" fill="##ff7f00"></path>
<path d="M57.41 78.1A7.41 7.41 0 1150 70.7a7.39 7.39 0 017.41 7.4zm-2.14-14.89H44.81l-1.72-36h13.82z" fill="#fff"></path>
</svg>
另外 SVG 既可以改变颜色,也可以任意放大缩小
SVG 可以使用 SVGO 来优化
使用纯样式替换图像
比如下面这个 loading 效果就是纯样式写的
相对于图像来说,纯代码的字节数就少得多了
@keyframes spin {
to {
transform: rotate(1turn);
}
}
.loading {
animation: spin 1.2s infinite linear;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #46aaff;
border-radius: 50%;
height: 30px;
width: 30px;
}
<div class="loading"></div>