关于性能优化:2022-前端性能优化最佳实践

28次阅读

共计 20842 个字符,预计需要花费 53 分钟才能阅读完成。

背景

随着业务的一直迭代,我的项目日渐壮大,为了给用户提供更优的体验,性能优化是前端开发避不开的话题。一个优良的网站必然是领有丰盛性能的同时具备比拟块的响应速度,想必咱们浏览网页时都更喜爱丝般顺滑的感触。

最近在学习整顿前端性能优化方面的常识,看了很多的文章,感觉文章多了比拟零散,学习效率不高,所以在浏览和学习其余优良博客文章的同时本人做了整顿和演绎,与大家一起学习和共勉。

本文相干图文内容多来自于稀土掘金社区,援用参考文献出处均在文章开端显著标注。

如有侵权,分割删除

一、性能优化的实质

性能优化的目标,就是为了提供给用户更好的体验,这些体验蕴含这几个方面:展现更快 交互响应快 页面无卡顿状况

更具体的说,就是指,在用户输出 url 到站点残缺把整个页面展现进去的过程中,通过各种优化策略和办法,让页面加载更快;在用户应用过程中,让用户的操作响应更及时,有更好的用户体验。

对于前端工程师来说,要做好性能优化,须要了解浏览器加载和渲染的实质。了解了实质原理,能力更好的去做优化。

二、雅虎性能优化军规

雅虎军规是雅虎的开发人员在总结了网站的不合理局部后,提出的优化网站性能进步的一套办法规定,非常适合初学者绕过这些坎。十分简要,供大家参考应用,心愿对你们当前的开发过程中有所帮忙。

三、性能优化指标

3.1 以用户为核心的性能指标

  • First Paint 首次绘制(FP)
    这个指标用于记录页面第一次绘制像素的工夫,如显示页面背景色。

    FP 不蕴含默认背景绘制,但蕴含非默认的背景绘制。

  • First contentful paint 首次内容绘制 (FCP)
    LCP 是指页面开始加载到最大文本块内容或图片显示在页面中的工夫。如果 FP 及 FCP 两指标在 2 秒内实现的话咱们的页面就算体验优良。
  • Largest contentful paint 最大内容绘制 (LCP)
    用于记录视窗内最大的元素绘制的工夫,该工夫会随着页面渲染变动而变动,因为页面中的最大元素在渲染过程中可能会产生扭转,另外该指标会在用户第一次交互后进行记录。官网举荐的工夫区间,在 2.5 秒内示意体验优良
  • First input delay 首次输出提早 (FID)
    首次输出提早,FID(First Input Delay),记录在 FCP 和 TTI 之间用户首次与页面交互时响应的提早。
  • Time to Interactive 可交互工夫 (TTI)
    首次可交互工夫,TTI(Time to Interactive)。这个指标计算过程稍微简单,它须要满足以下几个条件:

    1. 从 FCP 指标后开始计算
    2. 继续 5 秒内无长工作(执行工夫超过 50 ms)且无两个以上正在进行中的 GET 申请
    3. 往前回溯至 5 秒前的最初一个长工作完结的工夫

    对于用户交互(比方点击事件),举荐的响应工夫是 100ms 以内。那么为了达成这个指标,举荐在闲暇工夫里执行工作不超过 50ms(W3C 也有这样的标准规定),这样能在用户无感知的状况下响应用户的交互,否则就会造成提早感。

  • Total blocking time 总阻塞工夫 (TBT)
    阻塞总工夫,TBT(Total Blocking Time),记录在 FCP 到 TTI 之间所有长工作的阻塞工夫总和。
  • Cumulative layout shift 累积布局偏移 (CLS)
    累计位移偏移,CLS(Cumulative Layout Shift),记录了页面上非预期的位移稳定。页面渲染过程中忽然插入一张微小的图片或者说点击了某个按钮忽然动静插入了一块内容等等相当影响用户体验的网站。这个指标就是为这种状况而生的,计算形式为:位移影响的面积 * 位移间隔。

3.2 三大外围指标(Core Web Vitals)

Google 在 20 年五月提出了网站用户体验的三大外围指标

3.2.1 Largest Contentful Paint (LCP)

LCP 代表了页面的速度指标,尽管还存在其余的一些体现速度的指标,然而上文也说过 LCP 能体现的货色更多一些。一是指标实时更新,数据更准确,二是代表着页面最大元素的渲染工夫,通常来说页面中最大元素的疾速载入能让用户感觉性能还挺好。

那么哪些元素能够被定义为最大元素呢?

  • <img> 标签
  • <image> 在 svg 中的 image 标签
  • <video> video 标签
  • CSS background url()加载的图片
  • 蕴含内联或文本的块级元素

线上测量工具

  • Chrome User Experience Report
  • PageSpeed Insights
  • Search Console (Core Web Vitals report)
  • web-vitals JavaScript library

实验室工具

  • Chrome DevTools
  • Lighthouse
  • WebPageTest

原生的 JS API 测量
LCP 还能够用 JS API 进行测量,次要应用PerformanceObserver 接口,目前除了 IE 不反对,其余浏览器根本都反对了。

new PerformanceObserver((entryList) => {for (const entry of entryList.getEntries()) {console.log('LCP candidate:', entry.startTime, entry);
  }
}).observe({type: 'largest-contentful-paint', buffered: true});

如何优化 LCP
LCP 可能被这四个因素影响:

  • 服务端响应工夫
  • Javascript 和 CSS 引起的渲染卡顿
  • 资源加载工夫
  • 客户端渲染

3.2.2 First Input Delay (FID)

FID 代表了页面的交互体验指标,毕竟没有一个用户心愿触发交互当前页面的反馈很缓慢,交互响应的快会让用户感觉网页挺晦涩。

这个指标其实挺好了解,就是看用户交互事件触发到页面响应两头耗时多少,如果其中有长工作产生的话那么势必会造成响应工夫变长。举荐响应用户交互在 100ms 以内.

线上测量工具

  • Chrome User Experience Report
  • PageSpeed Insights
  • Search Console (Core Web Vitals report)
  • web-vitals JavaScript library

原生的 JS API 测量

new PerformanceObserver((entryList) => {for (const entry of entryList.getEntries()) {
    const delay = entry.processingStart - entry.startTime;
    console.log('FID candidate:', delay, entry);
  }
}).observe({type: 'first-input', buffered: true});

如何优化 FID
FID 可能被这四个因素影响:

  • 缩小第三方代码的影响
  • 缩小 Javascript 的执行工夫
  • 最小化主线程工作
  • 减小申请数量和申请文件大小

3.2.3 Cumulative Layout Shift (CLS)

CLS 代表了页面的稳固指标,它能掂量页面是否排版稳固。尤其在手机上这个指标更为重要,因为手机屏幕挺小,CLS值一大的话会让用户感觉页面体验做的很差。CLS 的分数在 0.1 或以下,则为 Good。

浏览器会监控两桢之间产生挪动的不稳固元素。布局挪动分数由 2 个元素决定:impact fractiondistance fraction

layout shift score = impact fraction * distance fraction

上面例子中,竖向间隔更大,该元素绝对适口挪动了 25% 的间隔,所以 distance fraction 是 0.25。所以布局挪动分数是 0.75 * 0.25 = 0.1875

然而要留神的是,并不是所有的布局挪动都是不好的,很多 web 网站都会扭转元素的开始地位。只有当布局挪动是非用户预期的,才是不好的

换句话说,当用户点击了按钮,布局进行了改变,这是 ok 的,CLS 的 JS API 中有一个字段 hadRecentInput,用来标识 500ms 内是否有用户数据,视状况而定,能够疏忽这个计算。

线上测量工具

  • Chrome User Experience Report
  • PageSpeed Insights
  • Search Console (Core Web Vitals report)
  • web-vitals JavaScript library

实验室工具

  • Chrome DevTools
  • Lighthouse
  • WebPageTest

原生的 JS API 测量
let cls = 0;

new PerformanceObserver((entryList) => {for (const entry of entryList.getEntries()) {if (!entry.hadRecentInput) {
      cls += entry.value;
      console.log(‘Current CLS value:’, cls, entry);
    }
  }
}).observe({type:‘layout-shift’, buffered: true});

如何优化 CLS
咱们能够依据这些准则来防止非预期布局挪动:

  • 图片或视屏元素有大小属性,或者给他们保留一个空间大小,设置 width、height,或者应用 unsized-media feature policy。
  • 不要在一个已存在的元素下面插入内容,除了相应用户输出。
  • 应用 animation 或 transition 而不是间接触发布局扭转。

3.3 性能工具:工欲善其事,必先利其器

Google 开发的 所有工具 都反对 Core Web Vitals 的测量。工具如下:

  • Lighthouse
  • PageSpeed Insights
  • Chrome DevTools
  • Search Console
  • web.dev’s 提供的测量工具
  • Web Vitals 扩大
  • Chrome UX Report API

工具:思考与总结
咱们该如何抉择?如何应用好这些工具进行剖析?

  • 首先咱们能够应用 Lighthouse,在本地进行测量,依据报告给出的一些倡议进行优化;
  • 公布之后,咱们能够应用 PageSpeed Insights 去看下线上的性能状况;
  • 接着,咱们能够应用 Chrome User Experience Report API 去捞取线上过来 28 天的数据;
  • 发现数据有异样,咱们能够应用 DevTools 工具进行具体代码定位剖析;
  • 应用 Search Console’s Core Web Vitals report 查看网站性能整体状况;
  • 应用 Web Vitals 扩大不便的看页面外围指标状况;

四、HTTP 中的性能优化

4.1 HTTP 1.1

HTTP/1.1 中大多数的网站性能优化技术都是缩小向服务器发动的 HTTP 申请数。浏览器能够同时建设无限个 TCP 连贯,而通过这些连贯下载资源是一个线性的流程:一个资源的申请响应返回后,下一个申请能力发送。这被称为线头阻塞。

在 HTTP/1.1 中,Web 开发者往往将整个网站的所有 CSS 都合并到一个文件。相似的,JavaScript 也被压缩到了一个文件,图片被合并到了一张雪碧图上。合并 CSS、JavaScript 和图片极大地缩小了 HTTP 的申请数,在 HTTP/1.1 中能取得显著的性能晋升。

存在的问题:
为了尽可能减少申请数,须要做合并文件、雪碧图、资源内联等优化工作,然而这无疑造成了单个申请内容变大提早变高的问题,且内嵌的资源不能无效地应用缓存机制。

4.2 HTTP/2.0 的劣势

4.2.1 二进制分帧传输

帧是数据传输的最小单位,以二进制传输代替本来的明文传输,本来的报文音讯被划分为更小的数据帧。

原来 Headers + Body 的报文格式现在被拆分成了一个个二进制的帧,用 Headers 帧 寄存头部字段,Data 帧 寄存申请体数据。分帧之后,服务器看到的不再是一个个残缺的 HTTP 申请报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因而也就不会排队期待,也就没有了 HTTP 的队头阻塞问题。

4.2.2 多路复用(MultiPlexing)

通信单方都能够给对方发送二进制帧,这种二进制帧的 双向传输的序列 ,也叫做流(Stream)。HTTP/2 用流来在一个 TCP 连贯上来进行多个数据帧的通信,这就是 多路复用 的概念。

在一个 TCP 连贯上,咱们能够向对方一直发送帧,每帧的 Stream Identifier 表明这一帧属于哪个流,而后在对方接管时,依据 Stream Identifier 拼接每个流的所有帧组成一整块数据。把 HTTP/1.1 每个申请都当作一个流,那么多个申请变成多个流,申请响应数据分成多个帧,不同流中的帧交织地发送给对方,这就是 HTTP/2 中的多路复用。

流的概念实现了单连贯上多申请 – 响应并行,解决了线头阻塞的问题,缩小了 TCP 连贯数量和 TCP 连贯慢启动造成的问题。所以 http2 对于同一域名只须要创立一个连贯,而不是像 http/1.1 那样创立 6~8 个连贯

4.2.3 服务端推送(Server Push)

在 HTTP/2 当中,服务器曾经不再是齐全被动地接管申请,响应申请,它也能新建 stream 来给客户端发送音讯,当 TCP 连贯建设之后,比方浏览器申请一个 HTML 文件,服务器就能够在返回 HTML 的根底上,将 HTML 中援用到的其余资源文件一起返回给客户端,缩小客户端的期待。

Server-Push 次要是针对资源内联做出的优化,相较于 http/1.1 资源内联的劣势:

  • 客户端能够缓存推送的资源
  • 客户端能够拒收推送过去的资源
  • 推送资源能够由不同页面共享
  • 服务器能够依照优先级推送资源

4.2.4 Header 压缩(HPACK)

应用 HPACK 算法来压缩首部内容

4.3 HTTP/2 Web 优化最佳实际

HTTP/ 2 的优化须要不同的思维形式。Web 开发者应该专一于网站的 缓存调优 ,而不是放心如何缩小 HTTP 申请数。通用的法令是, 传输轻量、细粒度的资源,以便独立缓存和并行传输。

4.3.1 进行合并文件

在 HTTP/ 2 中合并文件不再是一项最佳实际。尽管合并仍然能够进步压缩率,但它带来了代价昂扬的缓存生效。即便有一行 CSS 扭转了,浏览器也会强制从新加载你 所有的 CSS 申明。

另外,你的网站不是所有页面都应用了合并后的 CSS 或 JavaScript 文件中的全副申明或函数。被缓存之后倒没什么关系,但这意味着在用户第一次拜访时这些不必要的字节被传输、解决、执行了。HTTP/1.1 中申请的开销使得这种衡量是值得的,而在 HTTP/ 2 中这实际上减慢了页面的首次绘制。

Web 开发者应该更加专一于缓存策略优化,而不是压缩文件。将常常改变和不怎么改变的文件拆散开来,就能够尽可能利用 CDN 或者用户浏览器缓存中已有的内容。

4.3.2 进行内联资源

内联资源是文件合并的一个特例。它指的是将 CSS 样式表、内部的 JavaScript 文件和图片间接嵌入 HTML 页面中。

4.3.3 进行细分域名

细分域名是让浏览器建设更多 TCP 连贯的通常伎俩。浏览器限度了单个服务器的连贯数量,然而通过将网站上的资源切分到几个域上,你能够取得额定的 TCP 连贯。它防止了线头阻塞,但也带来了显著的代价。

细分域名在 HTTP/ 2 中应该防止。每个细分的域名都会带来额定的 DNS 查问、TCP 连贯和 TLS 握手(假如服务器应用不同的 TLS 证书)。在 HTTP/1.1 中,这个开销通过资源的并行下载失去了弥补。但在 HTTP/ 2 中就不是这样了:多路复用使得多个资源能够在一个连贯中并行下载。同时,相似于资源内联,域名细分毁坏了 HTTP/ 2 的流优先级,因为浏览器不能跨域比拟优先级。

4.4 一些最佳实际仍然无效

侥幸的是,HTTP/ 2 没有扭转所有的 Web 优化形式。一些 HTTP/1.1 中的最佳实际在 HTTP/ 2 中仍然无效。剩下的文章探讨了这些技巧,无论你在 HTTP/1.1 还是 HTTP/ 2 优化都能用上。

4.4.1 缩小 DNS 查问工夫

在浏览器能够申请网站资源之前,它须要通过域名零碎 (DNS) 取得你的服务端 IP 地址。直到 DNS 响应前,用户看到的都是白屏。HTTP/ 2 优化了 Web 浏览器和服务器之间的通信形式,但它不会影响域名零碎的性能。
因为 DNS 查问的开销可能会很低廉,尤其是当你从根名字服务器开始查问时,最小化网站应用的 DNS 查问数依然是一个明智之举。应用 HTML 头部的 <link rel=‘dns-prefetch’href=‘…’/> 能够帮忙你提前获取 DNS 记录,但这不是万能的解决方案。

4.4.2 动态资源应用 CDN

内容散发网络(CDN)是一组散布在多个不同地理位置的 Web 服务器。咱们都晓得,当服务器离用户越远时,提早越高。CDN 就是为了解决这一问题,在多个地位部署服务器,让用户离服务器更近,从而缩短申请工夫。

4.4.3 利用浏览器缓存

你能够进一步利用内容散发网络,将资源存储在用户的本地浏览器缓存中,除了产生一个 304 Not Modified 响应之外,这防止了任何模式的数据在网络上传输。

4.4.4 最小化 HTTP 申请大小

只管 HTTP/ 2 的申请应用了多路复用技术,在线缆上传输数据依然须要工夫。同时,缩小须要传输的数据规模同样会带来益处。在申请端,这意味着尽可能多地最小化 cookie、URL 和查问字符串的大小。

4.4.5 最小化 HTTP 响应大小

当然了,另一端也是这样。作为 Web 开发者,你会心愿服务端的响应尽可能的小。你能够最小化 HTML、CSS 和 JavaScript 文件,优化图像,并通过 gzip 压缩资源。

4.4.6 缩小不必要的重定向

HTTP 301 和 302 重定向在迁徙到新平台或者从新设计网站时难以避免,但如有可能应该被去除。重定向会导致一圈额定的浏览器到服务端往返,这会减少不必要的提早。你应该特地注意重定向链,下面须要多个重定向能力达到目标地址。
像 301 和 302 这样的服务端重定向虽不现实,但也不是世界上最糟的事件。它们能够在本地被缓存,所以浏览器能够辨认重定向 URL,并且防止不必要的往返。元标签中的刷新 (如 <meta http-equiv=“refresh”…) 在另一方面开销更大,因为它们无奈被缓存,而且在特定浏览器中存在性能问题。

五、代码压缩

5.1 开启 gzip 压缩

gzipGNUzip 的缩写,最早用于 UNIX 零碎的文件压缩。HTTP 协定上的 gzip 编码是一种用来改良 web 应用程序性能的技术,Web 服务器和客户端(浏览器)必须独特反对 gzip。目前支流的浏览器,Chrome,firefox,IE 等都反对该协定。常见的服务器如 Apache,Nginx,IIS 同样反对,gzip压缩效率十分高,通常能够达到 70% 的压缩率,也就是说,如果你的网页有 30K,压缩之后就变成了 9K 左右

5.1.1 Nginx 配置

gzip  on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/javascript application/x-javascript application/xml application/json;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";

配置好重新启动 Nginx,当看到申请响应头中有 Content-Encoding: gzip,阐明传输压缩配置曾经失效,此时能够看到咱们申请文件的大小曾经压缩很多。

5.1.2 Node 服务端

以下咱们以服务端应用咱们相熟的 express 为例,开启 gzip 非常简单,相干步骤如下:

  • 装置:
npm install compression —save
  • 增加代码逻辑:
var compression = require('compression');
var app = express();
app.use(compression())
  • 重启服务,察看网络面板外面的 response header,如果看到如下红圈里的字段则表明 gzip 开启胜利:

5.2 Webpack 压缩

在 webpack 能够应用如下插件进行压缩:

  • JavaScript:UglifyPlugin
  • CSS:MiniCssExtractPlugin
  • HTML:HtmlWebpackPlugin

其实,咱们还能够做得更好。那就是应用 gzip 压缩。能够通过向 HTTP 申请头中的 Accept-Encoding 头增加 gzip 标识来开启这一性能。当然,服务器也得反对这一性能。

gzip 是目前最风行和最无效的压缩办法。举个例子,我用 Vue 开发的我的项目构建后生成的 app.js 文件大小为 1.4MB,应用 gzip 压缩后只有 573KB,体积缩小了将近 60%。

附上 webpack 和 node 配置 gzip 的应用办法。

下载插件

npm install compression-webpack-plugin —-save-dev
npm install compression

webpack 配置

const CompressionPlugin = require(‘compression-webpack-plugin’);

module.exports = {plugins: [new CompressionPlugin()],
}

node 配置

const compression = require(‘compression’)
// 在其余中间件前应用
app.use(compression())

六、JavaScript 中的性能优化

6.1 不要笼罩原生办法

无论你的 JavaScript 代码如何优化,都比不上原生办法。因为原生办法是用低级语言写的(C/C++),并且被编译成机器码,成为浏览器的一部分。当原生办法可用时,尽量应用它们,特地是数学运算和 DOM 操作。

6.2 应用事件委托(简化 DOM 操作)

<ul>
  <li> 苹果 </li>
  <li> 香蕉 </li>
  <li> 凤梨 </li>
</ul>

<script>
// good
document.querySelector('ul').onclick = (event) => {
  const target = event.target
  if (target.nodeName === 'LI') {console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) => {e.onclick = function() {console.log(this.innerHTML)
  }
}) 
</script>

6.3 JS 动画

尽量避免增加大量的 JS 动画,CSS3 动画和 Canvas 动画都比 JS 动画性能好。
应用 requestAnimationFrame 来代替 setTimeoutsetInterval,因为 requestAnimationFrame 能够在正确的工夫进行渲染,setTimeoutsetInterval 无奈保障渲染机会。不要在定时器外面绑定事件。

6.4 节流和防抖

6.4.1 防抖(debounce)

// 在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则从新计时。function debounce(func, delay) {
    let time = null;
    return function (...args) {
        const context = this;
        if (time) {clearTimeout(time);
        }
        time = setTimeout(() => {func.call(context, ...args);
        }, delay);
    };
}

6.4.2 节流(throttle)

// 规定在一个单位工夫内,只能触发一次函数。如果这个单位工夫内触发屡次函数,只有一次失效。function throttle(func, delay) {let prevTime = Date.now();
    return function (...args) {
        const context = this;
        let curTime = Date.now();
        if (curTime - prevTime > delay) {
            prevTime = curTime;
            func.call(context, ...args);
        }
    };
}

七、页面渲染优化

Webkit 渲染引擎流程:

  • 解决 HTML 并构建 DOM 树
  • 解决 CSS 构建 CSS 规定树(CSSOM)
  • 接着 JS 会通过 DOM Api 和 CSSOM Api 来操作 DOM Tree 和 CSS Rule Tree 将 DOM Tree 和 CSSOM Tree 合成一颗渲染树 Render Tree。
  • 依据渲染树来布局,计算每个节点的地位
  • 调用 GPU 绘制,合成图层,显示在屏幕上

7.1 防止 CSS、JS 阻塞

7.1.1 CSS 的阻塞

咱们提到 DOM 和 CSSOM 合力能力构建渲染树。这一点会给性能造成重大影响:默认状况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已解决的内容,即使 DOM 曾经解析结束了

只有当咱们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才退场,CSSOM 的构建才开始。 很多时候,DOM 不得不期待 CSSOM。因而咱们能够这样总结:

CSS 是阻塞渲染的资源。须要将它尽早、尽快地下载到客户端,以便缩短首次渲染的工夫。尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现动态资源加载速度的优化)

7.1.2 JS 的阻塞

JS 的作用在于批改,它帮忙咱们批改网页的方方面面:内容、款式以及它如何响应用户交互。这“方方面面”的批改,实质上都是对 DOM 和 CSSDOM 进行批改。因而 JS 的执行会阻止 CSSOM,在咱们不作显式申明的状况下,它也会阻塞 DOM。

JS 不仅能够读取和批改 DOM 属性,还能够读取和批改 CSSOM 属性,存在阻塞的 CSS 资源时,浏览器会提早 JS 的执行和 Render Tree 构建。

JS 引擎是独立于渲染引擎存在的。咱们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会间接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行结束,浏览器又会把控制权还给渲染引擎,持续 CSSOM 和 DOM 的构建。因而与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。

  1. 古代浏览器会并行加载 JS 文件。
  2. 加载或者执行 JS 时会阻塞对标签的解析,也就是阻塞了 DOM 树的造成,只有等到 JS 执行结束,浏览器才会持续解析标签。没有 DOM 树,浏览器就无奈渲染,所以当加载很大的 JS 文件时,能够看到页面很长时间是一片空白

之所以会阻塞对标签的解析是因为加载的 JS 中可能会创立,删除节点等,这些操作会对 DOM 树产生影响,如果不阻塞,等浏览器解析完标签生成 DOM 树后,JS 批改了某些节点,那么浏览器又得从新解析,而后生成 DOM 树,性能比拟差。

理论应用时,能够遵循上面 3 个准则:

  • CSS 资源优于 JavaScript 资源引入
  • JS 应尽量少影响 DOM 的构建

7.1.3 扭转 JS 阻塞的形式

defer(延缓)模式

defer 形式加载 script, 不会阻塞 HTML 解析,等到 DOM 生成结束且 script 加载结束再执行 JS。

<script defer></script>
async(异步)模式

async 属性示意异步执行引入的 JS,加载时不会阻塞 HTML 解析,然而加载实现后立马执行,此时依然会阻塞 load 事件。

<script async></script>

从利用的角度来说,个别当咱们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,咱们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行后果时,咱们会选用defer

7.2 应用字体图标 iconfont 代替图片图标

字体图标就是将图标制作成一个字体,应用时就跟字体一样,能够设置属性,例如 font-size、color 等等,十分不便。并且字体图标是矢量图,不会失真。还有一个长处是生成的文件特地小。

7.3 升高 CSS 选择器的复杂性

浏览器读取选择器,遵循的准则是从选择器的左边到右边读取。看个示例:

#block .text p {color: red;}
  1. 查找所有 P 元素。
  2. 查找后果 1 中的元素是否有类名为 text 的父元素
  3. 查找后果 2 中的元素是否有 id 为 block 的父元素

CSS 选择器优先级

内联 > ID 选择器 > 类选择器 > 标签选择器

依据以上两个信息能够得出结论:

  1. 缩小嵌套。后辈选择器的开销是最高的,因而咱们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能应用类来关联每一个标签元素
  2. 关注能够通过继承实现的属性,防止反复匹配反复定义
  3. 尽量应用高优先级的选择器,例如 ID 和类选择器。
  4. 防止应用通配符,只对须要用到的元素进行抉择

7.4 缩小重绘和回流

7.4.1 重绘 (Repaint)

当页面中元素款式的扭转并不影响它在文档流中的地位时(例如:color、background-color、visibility 等),浏览器会将新款式赋予给元素并从新绘制它,这个过程称为重绘

7.4.2 回流 (Reflow)

当 Render Tree 中局部或全副元素的尺寸、构造、或某些属性产生扭转时,浏览器从新渲染局部或全副文档的过程称为回流。

回流必将引起重绘,重绘不肯定会引起回流,回流比重绘的代价要更高。

7.4.3 如何防止

CSS

  • 防止应用 table 布局。
  • 尽可能在 DOM 树的最末端扭转 class。
  • 防止设置多层内联款式。
  • 将动画成果利用到 position 属性为 absolute 或 fixed 的元素上。
  • 防止应用 CSS 表达式(例如:calc())。

JavaScript

  • 防止频繁操作款式,最好一次性重写 style 属性,或者将款式列表定义为 class 并一次性更改 class 属性。
  • 防止频繁操作 DOM,创立一个 documentFragment,在它下面利用所有 DOM 操作,最初再把它增加到文档中。
  • 也能够先为元素设置 display: none,操作完结后再把它显示进去。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 防止频繁读取会引发回流 / 重绘的属性,如果的确须要屡次应用,就用一个变量缓存起来。
  • 对具备简单动画的元素应用相对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

7.5 应用 flexbox 而不是较早的布局模型

在晚期的 CSS 布局形式中咱们能对元素履行相对定位、绝对定位或浮动定位。而当初,咱们有了新的布局形式 flexbox,它比起晚期的布局形式来说有个劣势,那就是性能比拟好。

7.6 图片资源优化

7.6.1 应用雪碧图

雪碧图的作用就是缩小申请数,而且多张图片合在一起后的体积会少于多张图片的体积总和,这也是比拟通用的图片压缩计划

7.6.2 升高图片品质

压缩办法有两种,一是通过在线网站进行压缩,二是通过 webpack 插件 image-webpack-loader。它是基于 imagemin 这个 Node 库来实现图片压缩的。

应用很简略,咱们只有在 file-loader 之后退出 image-webpack-loader 即可:

npm i -D image-webpack-loader

webpack 配置如下

// config/webpack.base.js
// ...
module: {
    rules: [
        {test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
            use: [
                {
                    loader: 'file-loader',
                    options: {name: '[name]_[hash].[ext]',
                        outputPath: 'images/'
                    }
                },
                {
                    loader: 'image-webpack-loader',
                    options: {
                        // 压缩 jpeg 的配置
                        mozjpeg: {
                            progressive: true,
                            quality: 65
                        },
                        // 应用 imagemin**-optipng 压缩 png,enable: false 为敞开
                        optipng: {enabled: false},
                        // 应用 imagemin-pngquant 压缩 png
                        pngquant: {
                            quality: '65-90',
                            speed: 4
                        },
                        // 压缩 gif 的配置
                        gifsicle: {interlaced: false},
                        // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格局
                        webp: {quality: 75}
                    }
                }
            ]
        }
    ];
}
// ...

7.6.3 图片懒加载

在页面中,先不给图片设置门路,只有当图片呈现在浏览器的可视区域时,才去加载真正的图片,这就是提早加载。对于图片很多的网站来说,一次性加载全副图片,会对用户体验造成很大的影响,所以须要应用图片提早加载。

7.6.4 应用 CSS3 代替图片

有很多图片应用 CSS 成果(突变、暗影等)就能画进去,这种状况抉择 CSS3 成果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。

7.6.5 应用 webp 格局的图片

WebP 是 Google 团队开发的放慢图片加载速度的图片格式,其劣势体现在它具备更优的图像数据压缩算法,能带来更小的图片体积,而且领有肉眼辨认无差别的图像品质;同时具备了无损和有损的压缩模式、Alpha 通明以及动画的个性,在 JPEG 和 PNG 上的转化成果都相当优良、稳固和对立。

八、Webpack 优化

8.1 缩小 ES6 转为 ES5 的冗余代码

Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如上面的 ES6 代码:

class HelloWebpack extends Component{...}

这段代码再被转换成能失常运行的 ES5 代码时须要以下两个辅助函数:

babel-runtime/helpers/createClass  // 用于实现 class 语法
babel-runtime/helpers/inherits  // 用于实现 extends 语法    

在默认状况下,Babel 会在每个输入文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会呈现很屡次,造成代码冗余。为了不让这些辅助函数的代码反复呈现,能够在依赖它们时通过 require('babel-runtime/helpers/createClass') 的形式导入,这样就能做到只让它们呈现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相干辅助函数进行替换成导入语句,从而减小 babel 编译进去的代码的文件大小。

首先,装置 babel-plugin-transform-runtime

npm install babel-plugin-transform-runtime —save-dev

而后,批改 .babelrc 配置文件为:

"plugins": ["transform-runtime"]

如果要看插件的更多具体内容,能够查看babel-plugin-transform-runtime 的 具体介绍。

8.2 提取公共代码

如果我的项目中没有去将每个页面的第三方库和公共模块提取进去,则我的项目会存在以下问题:

  • 雷同的资源被反复加载,节约用户的流量和服务器的老本。
  • 每个页面须要加载的资源太大,导致网页首屏加载迟缓,影响用户体验。

所以咱们须要将多个页面的公共代码抽离成独自的文件,来优化以上问题。Webpack 内置了专门用于提取多个 Chunk 中的公共局部的插件 CommonsChunkPlugin,咱们在我的项目中 CommonsChunkPlugin 的配置如下:

// 所有在 package.json 外面依赖的包,都会被打包进 vendor.js 这个文件中。new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: function(module, count) {
    return (
      module.resource &&
      /\.js$/.test(module.resource) &&
      module.resource.indexOf(path.join(__dirname, '../node_modules')
      ) === 0
    );
  }
}),
// 抽取出代码模块的映射关系
new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  chunks: ['vendor']
})

如果要看插件的更多具体内容,能够查看 CommonsChunkPlugin 的 具体介绍。

8.3 模板预编译

当应用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常状况下这个过程曾经足够快了,但对性能敏感的利用还是最好防止这种用法。
预编译模板最简略的形式就是应用 单文件组件 ——相干的构建设置会主动把预编译解决好,所以构建好的代码曾经蕴含了编译进去的渲染函数而不是原始的模板字符串。
如果你应用 webpack,并且喜爱拆散 JavaScript 和模板文件,你能够应用 vue-template-loader,它也能够在构建过程中把模板文件转换成为 JavaScript 渲染函数。

8.4 提取组件的 CSS

当应用单文件组件时,组件内的 CSS 会以 style 标签的形式通过 JavaScript 动静注入。这有一些小小的运行时开销,如果你应用服务端渲染,这会导致一段“无款式内容闪动 (fouc)”。将所有组件的 CSS 提取到同一个文件能够防止这个问题,也会让 CSS 更好地进行压缩和缓存。
查阅这个构建工具各自的文档来理解更多:

  • webpack + vue-loader (vue-cli 的 webpack 模板曾经事后配置好)
  • Browserify + vueify
  • Rollup + rollup-plugin-vue

8.5 按需加载代码

通过 Vue 写的单页利用时,可能会有很多的路由引入。当打包构建的时候,JS 包会变得十分大,影响加载。如果咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,然而可能其余的页面的速度就会降下来。
我的项目中路由按需加载(懒加载)的配置:

const Foo = () => import('./Foo.vue')
const router = new VueRouter({
  routes: [{ path: '/foo', component: Foo}
  ]
})

九、Vue 我的项目性能优化

9.1 正当应用 v-ifv-show

v-if真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是 惰性的 :如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简略得多,不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的 display 属性进行切换。
所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。

9.2 正当应用 watchcomputed

computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值产生扭转,下一次获取 computed 的值时才会从新计算 computed 的值;
watch: 更多的是「察看」的作用,相似于某些数据的监听回调,每当监听的数据变动时都会执行回调进行后续操作;

使用场景:

  • 当咱们须要进行数值计算,并且依赖于其它数据时,应该应用 computed,因为能够利用 computed 的缓存个性,防止每次获取值时,都要从新计算;
  • 当咱们须要在数据变动时执行 异步 开销较大 的操作时,应该应用 watch,应用 watch 选项容许咱们执行异步操作 (拜访一个 API),限度咱们执行该操作的频率,并在咱们失去最终后果前,设置中间状态。这些都是计算属性无奈做到的。

9.3 v-for 遍历必须为 item 增加 key,且防止同时应用 v-if

9.3.1 v-for 遍历必须为 item 增加 key

在列表数据进行遍历渲染时,须要为每一项 item 设置惟一 key 值,不便 Vue.js 外部机制精准找到该条列表数据。当 state 更新时,新的状态值和旧的状态值比照,较快地定位到 diff。

9.3.2 v-for 遍历防止同时应用 v-if

v-forv-if 优先级高,如果每一次都须要遍历整个数组,将会影响速度,尤其是当之须要渲染很小一部分的时候,必要状况下应该替换成 computed 属性。

举荐:

<ul>
  <li
    v-for="user in activeUsers"
    :key="user.id">
    {{user.name}}
  </li>
</ul>
computed: {activeUsers: function () {return this.users.filter(function (user) {return user.isActive})
  }
}

不举荐:

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id">
    {{user.name}}
  </li>
</ul>

9.4 长列表性能优化

Vue 会通过 Object.defineProperty 对数据进行劫持,来实现视图响应数据的变动,然而有些时候咱们的组件就是纯正的数据展现,不会有任何扭转,咱们就不须要 Vue 来劫持咱们的数据,在大量数据展现的状况下,这可能很显著的缩小组件初始化的工夫,那如何禁止 Vue 劫持咱们的数据呢?能够通过 Object.freeze 办法来解冻一个对象,一旦被解冻的对象就再也不能被批改了。

export default {data: () => ({users: {}
  }),
  async created() {const users = await axios.get("/api/users");
    this.users = Object.freeze(users);
  }
};

9.5 事件的销毁

Vue 组件销毁时,会主动清理它与其它实例的连贯,解绑它的全副指令及事件监听器,然而仅限于组件自身的事件。如果在 JS 内应用 addEventListener 等形式是不会主动销毁的,咱们须要在组件销毁时手动移除这些事件的监听,免得造成内存泄露,如:

created() {addEventListener('click', this.click, false)
},
beforeDestroy() {removeEventListener('click', this.click, false)
}

9.6 图片资源懒加载

对于图片过多的页面,为了减速页面加载速度,所以很多时候咱们须要将页面内未呈现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的晋升,也进步了用户体验。

9.7 路由懒加载

Vue 是单页面利用,可能会有很多的路由引入,这样应用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会呈现白屏的状况,不利于用户体验。如果咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,然而可能其余的页面的速度就会降下来。
路由懒加载:

const Foo = () => import(‘./Foo.vue’)
const router = new VueRouter({
  routes: [{ path:‘/foo’, component: Foo}
  ]
})

9.8 第三方插件的按需引入

咱们在我的项目中常常会须要引入第三方插件,如果咱们间接引入整个插件,会导致我的项目的体积太大,咱们能够借助 babel-plugin-component,而后能够只引入须要的组件,以达到减小我的项目体积的目标。以下为我的项目中引入 element-ui 组件库为例:

  1. 首先,装置 babel-plugin-component
npm install babel-plugin-component -D
  1. 而后,将 .babelrc 批改为:
{"presets": [["es2015", { "modules": false}]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}
  1. 在 main.js 中引入局部组件:
import Vue from‘vue’;
import {Button, Select} from‘element-ui’;

 Vue.use(Button)
 Vue.use(Select)

9.9 优化有限列表性能

如果你的利用存在十分长或者有限滚动的列表,那么须要采纳 窗口化 的技术来优化性能,只须要渲染少部分区域的内容,缩小从新渲染组件和创立 dom 节点的工夫。你能够参考以下开源我的项目 vue-virtual-scroll-list 和 vue-virtual-scroller 来优化这种有限列表的场景的。

9.10 服务端渲染 SSR or 预渲染

如果你的我的项目的 SEO 和 首屏渲染是评估我的项目的要害指标,那么你的我的项目就须要服务端渲染来帮忙你实现最佳的初始加载性能和 SEO,具体的 Vue SSR 如何实现,能够参考作者的另一篇文章《Vue SSR 踩坑之旅》。如果你的 Vue 我的项目只需改善多数营销页面(例如 /,/about,/contact 等)的 SEO,那么你可能须要 预渲染,在构建时 (build time) 简略地生成针对特定路由的动态 HTML 文件。长处是设置预渲染更简略,并能够将你的前端作为一个齐全动态的站点,具体你能够应用 prerender-spa-plugin 就能够轻松地增加预渲染。

十、服务端渲染

10.1 实用场景

以下两种状况 SSR 能够提供很好的场景反对

  • 需更好的反对 SEO
    劣势在于 同步。搜索引擎爬虫是不会期待异步申请数据完结后再抓取信息的,如果 SEO 对应用程序至关重要,但你的页面又是异步申请数据,那 SSR 能够帮忙你很好的解决这个问题。
  • 需更快的达到工夫
    劣势在于 慢网络和运行迟缓的设施场景。传统 SPA 需残缺的 JS 下载实现才可执行,而 SSR 服务器渲染标记在服务端渲染 html 后即可显示,用户会更快的看到首屏渲染页面。如果首屏渲染工夫转化率对应用程序至关重要,那能够应用 SSR 来优化。

10.2 不实用场景

以下三种场景 SSR 应用须要谨慎

  • 同构资源的解决
    劣势在于程序须要具备通用性。联合 Vue 的钩子来说,能在 SSR 中调用的生命周期只有 beforeCreatecreated,这就导致在应用三方 API 时必须保障运行不报错。在三方库的援用时须要非凡解决使其反对服务端和客户端都可运行。
  • 部署构建配置资源的反对
    劣势在于运行环境繁多。程序需处于 node.js server 运行环境。
  • 服务器更多的缓存筹备
    劣势在于高流量场景需采纳缓存策略。利用代码需在双端运行解析,cpu 性能耗费更大,负载平衡和多场景缓存解决比 SPA 做更多筹备。

十一、缓存优化

11.1 浏览器缓存策略

缓存的意义就在于缩小申请,更多地应用本地的资源,给用户更好的体验的同时,也加重服务器压力。所以,最佳实际,就应该是尽可能命中强缓存,同时,能在更新版本的时候让客户端的缓存生效。

在更新版本之后,如何让用户第一工夫应用最新的资源文件呢?机智的前端们想出了一个办法,在更新版本的时候,顺便把动态资源的门路改了,这样,就相当于第一次拜访这些资源,就不会存在缓存的问题了

entry:{main: path.join(__dirname,'./main.js'),
    vendor: ['react', 'antd']
},
output:{path:path.join(__dirname,'./dist'),
    publicPath: '/dist/',
    filname: 'bundle.[chunkhash].js'
}

综上所述,咱们能够得出一个较为正当的缓存计划:

  • HTML:应用协商缓存。
  • CSS、JS 和图片:应用强缓存,文件命名带上 hash 值。

11.2 文件名哈希

Webpack 给咱们提供了三种哈希值计算形式,别离是 hashchunkhashcontenthash。那么这三者有什么区别呢?

  • hash:跟整个我的项目的构建相干,构建生成的文件 hash 值都是一样的,只有我的项目里有文件更改,整个我的项目构建的 hash 值都会更改。
  • chunkhash:依据不同的入口文件 (Entry) 进行依赖文件解析、构建对应的 chunk,生成对应的 hash 值。
  • contenthash:由文件内容产生的 hash 值,内容不同产生的 contenthash 值也不一样。
    显然,咱们是不会应用第一种的。改了一个文件,打包之后,其余文件的 hash 都变了,缓存天然都生效了。这不是咱们想要的。

chunkhashcontenthash的次要利用场景是什么呢?在理论在我的项目中,咱们个别会把我的项目中的 CSS 都抽离出对应的 CSS 文件来加以援用。如果咱们应用 chunkhash,当咱们改了 CSS 代码之后,会发现 CSS 文件hash 值扭转的同时,JS 文件的 hash 值也会扭转。这时候,contenthash就派上用场了。

十二、对于性能监控的思考

咱们在做性能优化的时候,经常会通过各种线上打点,来收集用户数据,进行性能剖析。没错,这是一种监控伎俩,更准确的说,这是一种”预先”监控伎俩。

”预先”监控诚然重要,但咱们也应该思考”事先”监控,否则,每次公布一个需要后,去线上看数据。咦,发现数据降落了,而后咱们去查代码,去查数据,去查起因。这样性能优化的同学永远处于”追赶者”的角色,永远跟在屁股前面查问题。

举个例子,咱们能够这样去做”事先”监控。

建设流水线机制。流水线上如何做呢?

  • Lighthouse CI 或 PageSpeed Insights API:把 Lighthouse 或 PageSpeed Insights API 集成到 CI 流水线中,输入报告剖析。
  • Puppeteer 或 Playwright:应用 E2E 自动化测试工具集成到流水线模仿用户操作,失去 Chrome Trace Files,也就是咱们平时录制 Performance 后,点击左上角下载的文件。Puppeteer 和 Playwright 底层都是基于 Chrome DevTools Protocol。

Chrome Trace Files:依据 规定 剖析 Trace 文件,能够失去每个函数执行的工夫。如果函数执行工夫超过了一个临界值,能够抛出异样。如果一个函数每次的执行工夫都超过了临界值,那么就值得注意了。然而还有一点须要思考的是:函数执行的工夫是否超过临界值诚然重要,但更重要的是这是不是用户的输出响应函数,与用户体验是否无关。

  • 输入报告。定义异样临界值。如果异样过多,思考是否卡公布流程。

十三、参考资料

Web Vitals
Aerotwist – The Anatomy of a Frame
Performance Timeline – Web API 接口参考 | MDN
Getting Around the Chromium Source Code Directory Structure
前端性能优化 24 条倡议(2020)– 掘金
【前端优化】首屏加载 9.2s 压缩至 3.6 s – 掘金
async vs defer attributes – Growing with the Web
我的前端性能优化常识体系 – 掘金
前端性能优化三部曲 (加载篇)
全链路前端性能优化 (欢送珍藏) – 掘金
性能优化到底应该怎么做 – 掘金
『Webpack 系列』—— 路由懒加载的原理 – 掘金
前端缓存最佳实际 – 掘金
『前端优化』—— Vue 我的项目性能优化 – 掘金
服务端渲染 SSR 及实现原理 – 掘金
Vue 我的项目性能优化 — 实际指南(网上最全 / 具体)– 掘金
Vue 我的项目 Webpack 优化实际,构建效率进步 50% – 掘金


我是佩奇烹饪家,年老的前端攻城狮,爱专研,爱技术,爱分享。
集体笔记,整顿不易,感激 浏览 点赞 关注 珍藏
文章有任何问题欢送大家指出,也欢送大家一起交流学习!

正文完
 0