Skip to content

本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com

作者:@gauseen[1]
可点击左下角【阅读原文】访问外链,提升阅读体验

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

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

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

  • 浏览器地址栏输入时联想提示

  • URL 解析,URL 为了在不同协议不同传输机制都可以安全的运送信息,采用的字符都是符合 ASCII 集的。若有非 ASCII 的字符(中文等),就会先通过转义处理

  • 客户端(浏览器)缓存机制 ✔

  • DNS (Domain Name System) 解析 ✔

  • 浏览器建立一条与 web 服务器的 TCP 连接(TCP 三次握手) ✔

  • 浏览器向服务器发送一条 http 请求报文 ✔

  • 服务器向浏览器回送一条 http 响应报文 ✔

  • 浏览器判断缓存 ✔

  • 浏览器接收、解析、渲染资源 ✔

  • 关闭连接(TCP 四次握手)

优化之浏览器缓存机制


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

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

  • 本地缓存

  • localStorage

  • sessionStorage

  • indexedDB

  • cookie

  • Service Worker

  • 什么是 Service Worker

  • HTTP 缓存

  • 优先级底

  • 协商缓存不管是否命中都要和服务器端发生交互

  • 响应头控制字段:Last-Modified && If-Modified-SinceETag && If-None-Match

  • 优先级高于协商缓存

  • 浏览器一旦命中强缓存,直接使用缓存,不会和服务器发生交互

  • 响应头控制字段:ExpiresCache-Control

  • 强缓存

  • 协商缓存

优化之 DNS 预解析


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

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

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

代码实现如下:

<!-- 打开和关闭 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 文件体积比较大,下载时间就会很长,容易造成浏览器堵塞,浏览器页面会呈现出 “白屏” 效果,用户会感觉浏览器“卡死了”,没有响应。浏览器允许脚本异步加载、执行。

<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 等),这样可以减少带宽,加快页面响应速度

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 项目中,我们通常以路由为维度,进行代码分割,进行按需加载。

// 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 能使浏览器开始渲染时间提前。

<style>  .main-container {    background-color: #ccc;  }</style>

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

异步加载 CSS

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

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

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

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

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

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

<!-- 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 重排、重绘的属性有很多,具体可参考这里

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 的请求,从而提高性能

/* 通过 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

  • WOFF 比较广泛的支持(推荐使用)

  • WOFF2 只有现代浏览器支持(推荐使用)

  • EOT 只有 IE 支持(IE9 以下)

  • TTF/OTF 部分 IE 、主流浏览器支持

  • SVG 字体兼容性很差,现在 Chrome 也放弃了对其支持,可忽略

预加载网页字体资源

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

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

@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 端都可以很好的展示页面。那就要考虑真的需要在移动端加载视频吗?!如若不需要可以使用如下方式解决:

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

压缩视频

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

按需加载

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

<!-- 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>
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 属性的支持。

参考

  • DNS 预读取 [2]

  • 前端性能优化之旅 [3]

  • 雅虎前端优化 35 条规则 [4]

  • ECMAScript 6 入门 [5]

  • PageSpeed Insights[6]

参考资料

[1]

@gauseen: _https://github.com/gauseen_

[2]

DNS 预读取: _https://developer.mozilla.org/zh-CN/docs/Controlling_DNS_prefetching_

[3]

前端性能优化之旅: _https://github.com/alienzhou/fe-performance-journey_

[4]

雅虎前端优化 35 条规则: _https://github.com/creeperyang/blog/issues/1_

[5]

ECMAScript 6 入门: _http://es6.ruanyifeng.com/#docs/module-loader#浏览器加载_

[6]

PageSpeed Insights: _https://developers.google.com/speed/docs/insights/EnableCompression_