Web性能优化之瘦身秘笈

Web 传输的内容当然是越少越好,最近一段时间的工作一直致力于 Web 性能优化,这是我近期使用过的一些缩减 Web 体积的手段

这些手段主要是为了减少 Web 传输的内容大小,只有干货

CSS

🐛 删除无用的样式

在使用 UI 库的时候,UI 库提供的样式并不是所有的都会使用到

例如一个 button 组件一般都会提供 default/primary/success/warning/danger 五颜六色好几款样式

buttons

但我们实际一个项目中也许只会用到其中的一两种款式,为了减少样式表的体积,需要将那些没有使用的样式挑选出来删除掉

使用 uncss 工具来删除无用的样式

该工具提供有在线版,只需要复制自己的 HTML 以及 CSS,点击按钮就可以生成精简后的样式

另外也可以通过浏览器工具 Coverage 挑选出未使用的样式,如下图

coverage

经过分析得出每个文件未使用样式的百分占比,其中红色标记的为未使用到的样式,从下图中可以看到具体未使用到的样式有哪些

coverage-style

⚠️ 上面两种方法都是通过样式规则的选择器在页面上查找元素,如果能找到对应的元素,则说明该样式规则有被使用,随着在页面上进行各种操作,该百分比可能会降低,因为有些样式会在某些操作执行之后才会被使用到,比如 :hover 伪类相关的样式,在鼠标移入元素之前不会被标记为已使用的

所以,这两种方式都有一定的局限性,并不是挑选出的样式就一定是没有用的,也许某个样式是在用户执行相当复杂的操作后才会起作用,需要严格测试

ℹ️ 许多框架和库也提供自定义打包版本,从源头舍去那些无用的代码

🐛 删除被层叠的样式

CSS 全名 层叠样式表(Cascading Style Sheets),对同一个元素多次指定同一个样式只会让优先级高的覆盖优先级低的

在样式规则的选择器完全相同的情况下(比如这里 .selector-1 > .selector-2 和 .selector-1 > .selector-2 是完全相同的),被覆盖的样式可以安全地删除,如下

style

通过浏览器的开发者工具可以轻松看到哪些样式被覆盖了

uncss

如果这两条样式规则是相邻的,还可以合并成一条,不过压缩工具会帮助完成这个事情,无需动手

⚠️ 在选择器不相同的时候,也有可能会匹配到同一个元素,这个时候本条规则并不适用,需要注意

⚠️ 有时候同一个样式属性反复出现只是为了兼容一些旧浏览器,也需要注意

🐛 按需使用复合属性

有些样式属性可以合并为一条,比如

.selector {
  flex-direction: column;
  flex-wrap: wrap;
}
/* 合并后 */
.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

该工具还提供了 在线版

压缩前后对比,这就是他主页上的示例

/* normalize selectors */
h1::before, h1:before {
    /* reduce shorthand even further */
    margin: 10px 20px 10px 20px;
    /* reduce color values */
    color: #ff0000;
    /* remove duplicated properties */
    font-weight: 400;
    font-weight: 400;
    /* reduce position values */
    background-position: bottom right;
    /* normalize wrapping quotes */
    quotes: '«' "»";
    /* reduce gradient parameters */
    background: linear-gradient(to bottom, #ffe500 0%, #ffe500 50%, #121 50%, #121 100%);
    /* replace initial values */
    min-width: initial;
}
/* correct invalid placement */
@charset "utf-8";
@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 属于 PostCSS 生态系统,并且自带 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,浏览器支持范围最广的格式

根据目标设备选择合适的字体格式,不同的字体格式兼容的浏览器也是不一样的

下图是图一套字体的不同文件格式的大小对比

fonts

我们应该优先选用压缩率更高的 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 可以打开并编辑字体以及保存为其它格式

这是经过我编辑过后的文件大小对比,文件大小差距很大,确实用到的字体比较少

fonts-edit

图像

在 Web 网页中,图像的体积才是占了大部分,减少图像可以大幅改进性能

🐛 选择适合的图像格式

不同文件格式的图像其文件大小,图像质量是不一样的,根据具体情况选择合适的图像格式

常用 Web 图像格式

格式 透明 动画 说明 浏览器支持
GIF ✔️ ✔️ 颜色较少
JPEG 有损格式,常用于照片
PNG ✔️ 无损
WebP ✔️ ✔️ 支持无损/有损压缩,比JPEG,PNG和GIF更好的压缩效果 较新
AVIF ✔️ ✔️ 比 WebP,JPEG,PNG 和 GIF 更好的压缩效果 最新
JPEGXL ✔️ ✔️ 无损压缩,更快的解码和其他各种改进 暂无

ℹ️ 我写这篇博客的时候暂时没有浏览器支持 JPEGXL 格式的图像

ℹ️ 转换图像格式可以使用 Sqoosh 这个在线工具,在图像大小和质量之间手动调整权衡,并不是说优秀的图像格式体积就一定小

🐛 使用 WebP

一些新的图像格式拥有较高的性能,比如 AVIF 和 WebP

不过这些新的图像格式不是所有浏览器都支持,此时可以使用一个 <picture> 元素来包裹 <img> 元素,再通过使用 <source> 元素来为 <img> 元素提供多个备胎资源供其自行选择

<source> 元素可以有多个,srcset 属性是必须的(注意是 srcset)

<picture>
  <source srcset="logo.webp" type="image/webp">
  <img alt="logo" src="logo.png">
</picture>

值得一提的是 <picture> 元素内部必须包含一个 <img> 元素,否则图像不会显示(因为 <picture> 元素并不是一个独立显示的元素,而是单纯为 <img> 元素服务的)

还有 <img> 元素始终都不应该忘记的 alt 属性,当任何图像格式都无法显示或者图像下载失败的时候,至少还能显示替代的文字说明

要在 CSS 中使用 WebP 通常用 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 格式加载不同格式的图像了,<noscript> 的情况就要额外考虑了

注意,不要写成下面这样

.webp .logo {
  background: url(./logo.webp);
}

.logo {
  background: url(./logo.png);
}

这样写,虽然最终显示没有什么问题,但是对于支持 WebP 的浏览器来说就加载了多余的图像了,这与本文的主旨就背道而驰了

转换为 WebP 的在线工具 WebP Converter

或者在 libwebp 下载最新版本的 libwebp 工具,在其目录执行命令

.\bin\cwebp -q 80 logo.png -o logo.webp

其中参数 -q 表示图像质量

🐛 使用 AVIF

AVIF 有比 WebP 更优秀的性能,同样是 渐进增强 的方式来使用

picture

浏览器会自行忽略不支持的格式,如果浏览器支持 AVIF 格式就使用 logo.avif,退一步,如果支持 WebP 格式就使用 logo.webp

再退一步,如果上面俩都不支持,就会使用 logo.png 作为后备,如果不支持 <picture> 元素的浏览器会直接显示 <img> 元素

🐛 使用 JPEGXL

<picture> 元素也是面向未来而设计,等以后浏览器开始支持 JPEGXL 图像格式的时候就可以很方便的使用这种格式,甚至可能出现的其它类型图像格式

<picture>
  <source srcset="logo.jxl" type="image/jxl">
  <img alt="logo" src="logo.png">
</picture>

🐛 响应式图像

<img> 元素通过其新增的 srcsetsizes 属性来实现响应式图像

<img alt="avator"
  src="avator.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

浏览器宽度 设备像素比 显示哪张图像
<= 600px 1 avator-120.jpg
- 2 avator-240.jpg
- 4 avator-480.jpg
> 600px 1 avator-240.jpg
- 2 avator-480.jpg

ℹ️ 设备像素比也有可能是小数,比如 1.5,设备/浏览器会选择它自己认为最合适的那张图像来显示

ℹ️ 其中 src 属性是给不支持 srcsetsizes 属性的浏览器提供的降级方案

🐛 响应式的 AVIF 以及 WebP

将前面相关东西结合起来

<picture>
  <source
    sizes="(max-width: 600px) 120px, 240px"
    srcset="avator-120.avif 120w, avator-240.avif 240w, avator-480.avif 480w"
    type="image/avif"
  />
  <source
    sizes="(max-width: 600px) 120px, 240px"
    srcset="avator-120.webp 120w, avator-240.webp 240w, avator-480.webp 480w"
    type="image/webp"
  />
  <source
    sizes="(max-width: 600px) 120px, 240px"
    srcset="avator-120.jpg 120w, avator-240.jpg 240w, avator-480.jpg 480w"
    type="image/jpeg"
  />
  <img alt="avator" src="fallback-avator.jpg" width="120" height="120">
</picture>

放心,浏览器并不会加载不需要显示的图像

🐛 响应式背景图像

在 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 目前还在草案中,需要添加 -webkit- 前缀支持主流浏览器,而这应该使用 Autoprefixer 工具自动添加

⚠️ Safari 当前只支持图像的 url 路径和 1x/2x 这样的设备像素比描述符

🐛 图像压缩

有些格式的图像往往还会包含一些没有用的信息,清理掉这些信息有助于缩小图像体积

这通常使用工具来进行

使用 imagemin 压缩图像

🐛图像懒加载

页面上有很多图像我们一开始是看不到的,有的在我们滚动页面之后才会出现在屏幕上,又有的在某个对话框弹出后才能看到

对于这类图像,我们可以推迟它们的加载时机,等到它们需要真正展示在屏幕上的时候才加载,而不是在页面一开始时就加载,这将大大节省页面初始化时加载的资源大小

使用浏览器原生的懒加载方案,这非常简单,只需要给 <img> 元素添加一个 loading="lazy" 属性即可

<img alt="avator" loading="lazy" src="avator.jpg">

<picture> 元素搭配使用没问题,注意 loading<img> 元素的属性,不要写错位置

<picture>
  <source type="image/avif" srcset="avator.avif">
  <source type="image/webp" srcset="avator.webp">
  <img alt="avator" loading="lazy" src="avator.jpg">
</picture>

目前该属性只得到一部分浏览器的支持,不支持的浏览器会自行忽略

caniuse/loading-lazy-attr

该属性的 polyfill

还可以混合使用 JavaScript 插件,首先要判断 <img> 元素是否支持该属性

if ('loading' in HTMLImageElement.prototype) {
  // 支持,使用浏览器懒加载方案
} else {
  // 不支持,引入插件
}

插件我们这里使用 lazysizes,注意这里去掉了 <img> 元素的原本的 src 属性,改成了 data-src,并且添加了 class="lazyload"

如果浏览器支持,获取所有需要懒加载的图像元素,将这些元素 data-src 的值赋给 src

如果浏览器不支持,则加载并执行 lazysizes 插件

<img alt="avator" class="lazyload" data-src="avator.jpg" loading="lazy">

<script>
;(function() {
  if ('loading' in HTMLImageElement.prototype) {
    var images = document.querySelectorAll('img.lazyload')
    images.forEach(function(img) {
      img.src = img.dataset.src
    })
  } else {
    var script = document.createElement('script')
    script.async = true
    script.src = '//afarkas.github.io/lazysizes/lazysizes.min.js'
    document.body.appendChild(script)
  }
})()
</script>

lazysizes 插件会自动选取那些 class="lazyload"<img> 元素进行处理

⚠️ 引入一个插件会增加 JavaScript 的代码量,但是延迟了部分图像的加载时机,具体需要权衡

🐛 使用其它方案替换图像

减少图像最好的办法就是没有图像

使用 SVG 替换图像

warning

上面这张图像格式为 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 效果就是纯样式写的

相对于图像来说,纯代码的字节数就少得多了

<div class="loading"></div>
@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;
}
posted @ 2021-03-01 14:04  by.Genesis  阅读(2489)  评论(2编辑  收藏  举报