前端性能优化怎么做?

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

代码实现如下:

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

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

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

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

js 复制代码
<style>  
.main-container {  
background-color: #ccc;  
}  
</style>

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

异步加载 CSS

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

js 复制代码
DOM-tree(html) + CSSOM-tree(css) = Render-tree

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

js 复制代码
<link rel="preload" href="style/bigger.css" as="style" onload="this.rel='stylesheet'">

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

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

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

js 复制代码
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 的请求,从而提高性能

js 复制代码
/* 通过 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 也放弃了对其支持,可忽略

预加载网页字体资源

js 复制代码
<head>  
<link rel="preload" href="fonts/awesome.woff2" as="font">  
</head>

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

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

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

压缩视频

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

按需加载

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

js 复制代码
<!-- 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>
js 复制代码
document.addEventListener('DOMContentLoaded', function () {  
var lazyVideos = [].slice.call(document.querySelectorAll('video.lazy'))  
  
if ('IntersectionObserver'inwindow) {  
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 属性的支持。

相关推荐
hackeroink28 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css