Skip to content

前端性能优化 #13

@gauseen

Description

@gauseen

前端最贴近用户。关于前端的性能优化,主要从 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 很可能在链接对应的东西出现之前就已经解析完毕。这能够减少用户点击链接时的延迟。

代码实现如下:

<!-- 打开和关闭 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 就会异步加载。也就是说,浏览器不会等待这个脚本下载、执行完毕后再向后执行,而是直接继续向后执行,可避免解析 Javascript 导致的页面被阻塞渲染。但两者会阻塞 window.onload 事件

deferasync 区别:

  • deferDOM 结构完全生成,以及其他脚本执行完成,才会执行(渲染完再执行)。有多个 defer 脚本时,会按照页面出现的顺序依次加载、执行。会阻塞 DOMContentLoaded 事件
  • 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

预加载网页字体资源

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

参考

Metadata

Metadata

Assignees

Labels

优化性能优化

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions