Fork me on GitHub

前端性能优化

前端最贴近用户。关于前端的性能优化,主要从 URL 输入到页面展示的整个过程来讨论。

先来回顾一下从 URL 输入到页面显示,经历了什么过程?

‘✔’ 代表该过程在前端可进行优化

  • 浏览器地址栏输入时联想提示
  • URL 解析,URL 为了在不同协议不同传输机制都可以安全的运送信息,采用的字符都是符合 ASCII 集的。若有非 ASCII 的字符(中文等),就会先通过转义处理
  • 客户端(浏览器)缓存机制 ✔
  • DNS (Domain Name System) 解析 ✔
  • 浏览器建立一条与 web 服务器的 TCP 连接(TCP 三次握手) ✔
  • 浏览器向服务器发送一条 http 请求报文 ✔
  • 服务器向浏览器回送一条 http 响应报文 ✔
  • 浏览器判断缓存 ✔
  • 浏览器接收、解析、渲染资源 ✔
  • 关闭连接(TCP 四次握手)

优化之浏览器缓存机制


为了减少客户端(浏览器)向服务端请求次数和带宽,可借助浏览器缓存机制进行优化处理,进而提升网页的访问速度

浏览器缓存也包含很多种类:

  • 本地缓存

    • localStorage
    • sessionStorage
    • indexedDB
    • cookie
  • Service Worker

  • HTTP 缓存

    • 强缓存
      • 优先级高于协商缓存
      • 浏览器一旦命中强缓存,直接使用缓存,不会和服务器发生交互
      • 响应头控制字段:ExpiresCache-Control
    • 协商缓存
      • 优先级底
      • 协商缓存不管是否命中都要和服务器端发生交互
      • 响应头控制字段:Last-Modified && If-Modified-SinceETag && If-None-Match

优化之 DNS 预解析


DNS(Domain Name System),即域名系统,就是根据域名查出 IP 地址。查询域名对应的 IP 地址需要时间,前端能做的就是减少域名解析的时间,DNS 预解析就可以做到这一点。

X-DNS-Prefetch-Control 头控制着浏览器的 DNS 预读取功能。 DNS 预读取是一项使浏览器主动去执行域名解析的功能,其范围包括文档的所有链接,无论是图片的,CSS 的,还是 JavaScript 等其他用户能够点击的 URL

因为预读取会在后台执行,所以 DNS 很可能在链接对应的东西出现之前就已经解析完毕。这能够减少用户点击链接时的延迟。

代码实现如下:

1
2
3
4
5
6
<!-- 打开和关闭 DNS 预读取 -->
<meta http-equiv="x-dns-prefetch-control" content="on">

<!-- 强制查询特定主机名 -->
<!-- 通过使用 rel 属性值为 link type 中的 dns-prefetch 的 <link> 标签来对特定域名进行预读取 -->
<link rel="dns-prefetch" href="//www.domain.com/">

加载静态资源优化原则


  • 减少首屏内容大小
  • 减少 HPPT 请求次数
  • 减少资源包体积大小
  • 异步加载
  • 利用浏览器缓存

优化之减少首屏内容大小

如果所需的数据量超出了初始拥塞窗口的限制(通常是 14.6kB 压缩后大小),系统就需要在服务器和用户的浏览器之间进行更多次的往返。如果用户使用的是延迟时间较长的网络(如移动网络),该问题可能会显著拖慢网页加载速度

为提高网页加载速度,请限制用于呈现网页首屏内容的数据(HTML 标签、图片、CSSJavaScript)的大小

优化之减少资源包体积大小(启用压缩功能)

所有现代浏览器都支持 gzip 压缩,启用 gzip 压缩可大幅缩减传输资源大小,从而缩短资源下载时间,减少首次白屏时间,提升用户体验

  • zip 像是一个打包器,能把多个多件打包压缩到一个 .zip 文件包中
  • gzip(GNU zip) 一次只对一个文件压缩

zipgzip(gz) 不兼容

gzip 对基于文本格式文件的压缩效果最好(如:CSSJavaScriptHTML),在压缩较大文件时往往可实现高达 70-90% 的压缩率,对已经压缩过的资源(如:图片)进行 gzip 压缩处理,效果很不好。

所以我们应该启用 gzip 压缩,gzip 压缩需要在静态服务器做简单配置,具体可参考这里

优化之 JavaScript


异步加载资源

默认情况下,浏览器是同步加载 JavaScript 脚本,解析 html 过程中,遇到 <script> 标签就会停下来,等脚本下载、解析、执行完后,再继续向下解析渲染。

如果 js 文件体积比较大,下载时间就会很长,容易造成浏览器堵塞,浏览器页面会呈现出“白屏”效果,用户会感觉浏览器“卡死了”,没有响应。浏览器允许脚本异步加载、执行。

1
2
<script src="path/to/home.js" defer></script>
<script src="path/to/home.js" async></script>

上面代码中,<script> 标签分别有 deferasync 属性,浏览器识别到这 2 个属性时 js 就会异步加载。也就是说,浏览器不会等待这个脚本下载、执行完毕后再向后执行,而是直接继续向后执行

deferasync 区别:

  • deferDOM 结构完全生成,以及其他脚本执行完成,才会执行(渲染完再执行)。有多个 defer 脚本时,会按照页面出现的顺序依次加载、执行
  • async:一旦下载完成,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染(下载完就执行)。有多个 async 脚本时,不能保证按照页面出现顺序加载、执行

按需加载

在访问的页面只加载需要的资源(js、css、html 等),这样可以减少带宽,加快页面响应速度

1
2
3
4
5
6
7
const importModule = (url) => {
const script = document.createElement('script')
script.src = url
document.querySelector('body').appendChild(script)
}

importModule('path/to/home.js')

如上代码便实现了按需加载 js 功能,在需要的时候调用 importModule() 即可加载对应的 js 资源

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了

在实际工作中我们常用动态导入 import() 来实现按需加载,vuejs 项目中,我们通常以路由为维度,进行代码分割,进行按需加载。

1
2
3
4
5
6
// vuejs router 片段
{
path: '/about',
name: 'about',
component: () => import('@/pages/About.vue'),
}

如上代码,就实现了按需加载对应页面资源,从而提升了首屏加载速度,提高了用户体验

减少代码体积

  • 压缩
  • 去除无用代码

避免 Long Task

Long Tasks 是指执行时间超过 50 毫秒的任务,这些 Long Tasks 在很长一段时间内独占 UI 线程并阻止其他关键任务执行(如:点击、输入行为),阻塞了主线程,严重影响用户体验。

对用户而言,任务耗时较长表现为页面卡顿(反应慢),所以应该避免 Long Tasks 任务

优化之 CSS


如上图所示,浏览器渲染的主要步骤可分为:

  • 处理 HTML 标记并构建 DOM
  • 处理 CSS 标记并构建 CSSOM
  • DOMCSSOM 合并成一个渲染树
  • 根据渲染树来布局,以计算每个节点的几何信息
  • 在多个层上绘制各个元素的内容、边框、颜色、图像等
  • 合成,由于页面的各部分可能被绘制到多层上面,需要合成来确保按正确顺序绘制到屏幕上,以便正确渲染页面

内联首屏关键 CSS

<head> 标签 <style> 中,内联 CSS 能使浏览器开始渲染时间提前。

1
2
3
4
5
<style>
.main-container {
background-color: #ccc;
}
</style>

如上代码,这种方式并不适用于内联较大的 CSS 文件。因为我们要遵守【减少首屏内容大小】优化原则

异步加载 CSS

默认情况下,CSS 阻塞浏览器渲染,这意味着未加载完毕 CSS 资源之前,浏览器将不会渲染任何已处理的内容。

1
DOM-tree(html) + CSSOM-tree(css) = Render-tree

从上面的公式可知,HTMLCSS 都是阻塞渲染的资源,HTML 是必不可少的,因为它承载着网页的内容,但 CSS 的必要性相对来说没有那么重(首屏关键 CSS 除外)。我们可以采用非关键性 CSS 异步加载的方式,来加快渲染树的构建。

1
<link rel="preload" href="style/bigger.css" as="style" onload="this.rel='stylesheet'">

如上代码,在支持 rel="preload" 属性的浏览器中,rel 属性会让浏览器获取样式,但加载完样式不会应用它。所以需要 onload="this.rel='stylesheet'",也就是加载完后应用 CSS

注:preload 有兼容性问题,可在这里查看

1
2
<!-- media 属性可以为其它任意值 -->
<link rel="stylesheet" href="style/bigger.css" media="backup" onload="this.media = 'all'">

如上代码,通过 <link> 标签更改 media="backup" 属性,浏览器会异步加载该样式但不会去应用,也就是说不会阻塞渲染树的构建,从而加快浏览器渲染。

注意:加载完后通过 onload="this.media = 'all'",将 media 属性值设置为 all,让浏览器后续解析、渲染。

二者区别:rel="preload"media="backup" 加载优先级要高

减少重排、重绘

关于影响 CSS 重排、重绘的属性有很多,具体可参考这里

1
2
3
DOM-tree(html)  |
+ | => Render-tree => Layout(布局) => Paint(绘制) => Composite(合成)
CSSOM-tree(css) |

如上为浏览器渲染关键步骤

  • 减少重排(Layout),又叫回流

    如果改变了元素的几何属性(如:width、height、margin 等等),影响页面布局,浏览器就会重新检查所有元素,然后自动重排页面

  • 减少重绘(Paint)

    如果改变了不会影响页面布局的属性(如:color、background-image 等等),浏览器就跳过布局步骤,然后自动重绘页面

优化之图片

压缩图片

GIFPNGJPEG 格式在整个互联网的图片流量中占 96%。请确保部署到生产之前,对图片资源进行压缩处理,有个很好用的图片压缩网站 TinyPNG,当然也有 Google 开源的 Squoosh 压缩工具。SVG 矢量图属于 XML 文本类型,所以可以进行 gzip 压缩后再使用。

雪碧图(Sprite)

一种网页图片应用处理方式,将一个页面涉及到的所有小型图片或者图标都合并到一张大图里面,这样就只需要加载这个一个图片,而不是很多个图片了,这样就减少了很多 http 的请求,从而提高性能

1
2
3
4
5
6
/* 通过 background-position 属性控制图片显示位置,从而显示雪碧图中指定区域 */

.demo {
background: url('path/to/sprite.png') no-repeat;
background-position: 10px, 10px;
}

内嵌 base64

先将图片转为 base64 字符串,将其内嵌到页面中,这样可以减少 http 请求次数。但是图片转成 base64 体积会增大将近 1/3,增大了 html、css 的体积,同时也失去了浏览器的缓存能力。一般用于首屏加载的小图标

图片懒加载

图片只加载视口内可看到的图片,视口外的其它图片先不加载,这样就可以大大减少带宽、流量。加快渲染速度,提高用户体验

可使用 IntersectionObserver api 实现图片懒加载,但只有主流浏览器支持,还是可以接受的。这里有阮老师的具体讲解

如果需要兼容一些老的浏览器,可使用 lazyestload,它是一个小而精巧的开源库,推荐使用

优化之 字体

目前(2019-08)网络上使用的字体格式主要有: EOT、 TTF/OTF、 WOFFWOFF2

预加载网页字体资源

1
2
3
<head>
<link rel="preload" href="fonts/awesome.woff2" as="font">
</head>

如上代码,可预加载网页字体资源。<link rel="preload"> 用于“提示”浏览器尽快加载指定资源,但不会告知浏览器如何使用该资源。需要与 @font-face 配合使用,才能让浏览器知道如何解析处理给定的字体资源。

1
2
3
4
5
6
7
8
9
10
@font-face {
font-family:'Awesome Font';
font-style: normal;
font-weight: 400;
src: local('Awesome Font'),
url('fonts/awesome.woff2') format('woff2'),
url('fonts/awesome.woff') format('woff'),
url('fonts/awesome.ttf') format('truetype'),
url('fonts/awesome.eot') format('embedded-opentype');
}

如上代码,在 src 中优先列出 local('Your Font Name') 可确保不会针对已安装的字体发出多余的 HTTP 请求

压缩字体

因为默认情况下不会对字体进行压缩处理,所以在部署生产之前,请确保字体已经过 gzip 压缩处理。

缓存字体

字体资源比较稳定,非常适合使用缓存策略来减少带宽,优化体验

优化之 视频

移除不必要的视频

如一个响应式网站,在移动端和 PC 端都可以很好的展示页面。那就要考虑真的需要在移动端加载视频吗?!如若不需要可以使用如下方式解决:

1
2
3
4
5
6
/* 视口小于 650px 时,不加载对应 class 选择器的视频 */
@media screen and (max-width: 650px) {
.video_ele {
display: none;
}
}

压缩视频

可以使用 FFmpeg 工具对视频进行压缩、转换等处理。如果播放的视频不需要声音,可以去掉音轨信息减少文件体积。

按需加载

与图片一样,视频也可以按需加载,即在可视区域内时,再加载对应视频资源

1
2
3
4
5
<!-- poster 占位符,视频播放之前展示 -->
<video autoplay lazy poster="poster-image.jpg">
<source data-src="awesome.webm" type="video/webm">
<source data-src="awesome.mp4" type="video/mp4">
</video>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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)
})
}
})

如上代码,用 IntersectionObserver api 简单实现了视频按需加载功能。跟图片懒加载类似,将可视区域的 <video> 元素的 data-src 属性更改为 src 属性。然后再调用 load 方法即可触发视频加载,当然需要有 autoplay 属性的支持。

欢迎关注无广告文章公众号:学前端

参考

-------------我是有底线哒-------------