关于前端:阿里前端大牛性能优化12条建议

8次阅读

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

性能优化是把双刃剑,有好的一面也有坏的一面。好的一面就是能晋升网站性能,坏的一面就是配置麻烦,或者要恪守的规定太多。并且某些性能优化规定并不实用所有场景,须要审慎应用,请读者带着批判性的眼光来浏览本文。

本文相干的优化倡议的援用材料出处均会在倡议前面给出,或者放在文末。

1. 缩小 HTTP 申请

一个残缺的 HTTP 申请须要经验 DNS 查找,TCP 握手,浏览器收回 HTTP 申请,服务器接管申请,服务器解决申请并发回响应,浏览器接管响应等过程。接下来看一个具体的例子帮忙了解 HTTP:

这是一个 HTTP 申请,申请的文件大小为 28.4KB。

名词解释:

  • Queueing: 在申请队列中的工夫。
  • Stalled: 从 TCP 连贯建设实现,到真正能够传输数据之间的时间差,此工夫包含代理协商工夫。
  • Proxy negotiation: 与代理服务器连贯进行协商所破费的工夫。
  • DNS Lookup: 执行 DNS 查找所破费的工夫,页面上的每个不同的域都须要进行 DNS 查找。
  • Initial Connection / Connecting: 建设连贯所破费的工夫,包含 TCP 握手 / 重试和协商 SSL。
  • SSL: 实现 SSL 握手所破费的工夫。
  • Request sent: 收回网络申请所破费的工夫,通常为一毫秒的工夫。
  • Waiting(TFFB): TFFB 是收回页面申请到接管到应答数据第一个字节的工夫总和,它蕴含了 DNS 解析工夫、TCP 连接时间、发送 HTTP 申请工夫和取得响应音讯第一个字节的工夫。
  • Content Download: 接管响应数据所破费的工夫。

从这个例子能够看出,真正下载数据的工夫占比为 13.05 / 204.16 = 6.39%,文件越小,这个比例越小,文件越大,比例就越高。这就是为什么要倡议将多个小文件合并为一个大文件,从而缩小 HTTP 申请次数的起因。

参考资料:

  • understanding-resource-timing

2. 应用 HTTP2

HTTP2 相比 HTTP1.1 有如下几个长处:

解析速度快

服务器解析 HTTP1.1 的申请时,必须一直地读入字节,直到遇到分隔符 CRLF 为止。而解析 HTTP2 的申请就不必这么麻烦,因为 HTTP2 是基于帧的协定,每个帧都有示意帧长度的字段。

多路复用

HTTP1.1 如果要同时发动多个申请,就得建设多个 TCP 连贯,因为一个 TCP 连贯同时只能解决一个 HTTP1.1 的申请。

在 HTTP2 上,多个申请能够共用一个 TCP 连贯,这称为多路复用。同一个申请和响应用一个流来示意,并有惟一的流 ID 来标识。多个申请和响应在 TCP 连贯中能够乱序发送,达到目的地后再通过流 ID 重新组建。

首部压缩

HTTP2 提供了首部压缩性能。

例如有如下两个申请:

:authority: unpkg.zhimg.com
:method: GET
:path: /za-js-sdk@2.16.0/dist/zap.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
:authority: zz.bdstatic.com
:method: GET
:path: /linksubmit/push.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36

从下面两个申请能够看进去,有很多数据都是反复的。如果能够把雷同的首部存储起来,仅发送它们之间不同的局部,就能够节俭不少的流量,放慢申请的工夫。

HTTP/2 在客户端和服务器端应用“首部表”来跟踪和存储之前发送的键-值对,对于雷同的数据,不再通过每次申请和响应发送。

上面再来看一个简化的例子,假如客户端按程序发送如下申请首部:

Header1:foo
Header2:bar
Header3:bat

当客户端发送申请时,它会依据首部值创立一张表:

索引 首部名称
62 Header1 foo
63 Header2 bar
64 Header3 bat

如果服务器收到了申请,它会照样创立一张表。当客户端发送下一个申请的时候,如果首部雷同,它能够间接发送这样的首部块:

62 63 64

服务器会查找先前建设的表格,并把这些数字还原成索引对应的残缺首部。

优先级

HTTP2 能够对比拟紧急的申请设置一个较高的优先级,服务器在收到这样的申请后,能够优先解决。

流量管制

因为一个 TCP 连贯流量带宽(依据客户端到服务器的网络带宽而定)是固定的,当有多个申请并发时,一个申请占的流量多,另一个申请占的流量就会少。流量管制能够对不同的流的流量进行准确管制。

服务器推送

HTTP2 新增的一个弱小的新性能,就是服务器能够对一个客户端申请发送多个响应。换句话说,除了对最后申请的响应外,服务器还能够额定向客户端推送资源,而无需客户端明确地申请。

例如当浏览器申请一个网站时,除了返回 HTML 页面外,服务器还能够依据 HTML 页面中的资源的 URL,来提前推送资源。

当初有很多网站曾经开始应用 HTTP2 了,例如知乎:

其中 h2 是指 HTTP2 协定,http/1.1 则是指 HTTP1.1 协定。

参考资料:

  • HTTP2 简介
  • 半小时搞懂 HTTP、HTTPS 和 HTTP2

3. 应用服务端渲染

客户端渲染: 获取 HTML 文件,依据须要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。

服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。

  • 长处:首屏渲染快,SEO 好。
  • 毛病:配置麻烦,减少了服务器的计算压力。

参考资料:

  • vue-ssr-demo
  • Vue.js 服务器端渲染指南

4. 动态资源应用 CDN

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

CDN 原理

当用户拜访一个网站时,如果没有 CDN,过程是这样的:

  1. 浏览器要将域名解析为 IP 地址,所以须要向本地 DNS 发出请求。
  2. 本地 DNS 顺次向根服务器、顶级域名服务器、权限服务器发出请求,失去网站服务器的 IP 地址。
  3. 本地 DNS 将 IP 地址发回给浏览器,浏览器向网站服务器 IP 地址发出请求并失去资源。

如果用户拜访的网站部署了 CDN,过程是这样的:

  1. 浏览器要将域名解析为 IP 地址,所以须要向本地 DNS 发出请求。
  2. 本地 DNS 顺次向根服务器、顶级域名服务器、权限服务器发出请求,失去全局负载平衡零碎(GSLB)的 IP 地址。
  3. 本地 DNS 再向 GSLB 发出请求,GSLB 的次要性能是依据本地 DNS 的 IP 地址判断用户的地位,筛选出间隔用户较近的本地负载平衡零碎(SLB),并将该 SLB 的 IP 地址作为后果返回给本地 DNS。
  4. 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。
  5. SLB 依据浏览器申请的资源和地址,选出最优的缓存服务器发回给浏览器。
  6. 浏览器再依据 SLB 发回的地址重定向到缓存服务器。
  7. 如果缓存服务器有浏览器须要的资源,就将资源发回给浏览器。如果没有,就向源服务器申请资源,再发给浏览器并缓存在本地。

参考资料:

  • CDN 是什么?应用 CDN 有什么劣势?
  • CDN 原理简析

5\. 将 CSS 放在文件头部,JavaScript 文件放在底部

所有放在 head 标签里的 CSS 和 JS 文件都会梗塞渲染。如果这些 CSS 和 JS 须要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。

那为什么 CSS 文件还要放在头部呢?

因为先加载 HTML 再加载 CSS,会让用户第一工夫看到的页面是没有款式的、“俊俏”的,为了防止这种状况产生,就要将 CSS 文件放在头部了。

另外,JS 文件也不是不能够放在头部,只有给 script 标签加上 defer 属性就能够了,异步下载,提早执行。

6\. 应用字体图标 iconfont 代替图片图标

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

压缩字体文件

应用 fontmin-webpack 插件对字体文件进行压缩(感激前端小伟提供)。

参考资料:

  • fontmin-webpack
  • Iconfont- 阿里巴巴矢量图标库

7\. 善用缓存,不反复加载雷同的资源

为了防止用户每次拜访网站都得申请文件,咱们能够通过增加 Expires 或 max-age 来管制这一行为。Expires 设置了一个工夫,只有在这个工夫之前,浏览器都不会申请文件,而是间接应用缓存。而 max-age 是一个绝对工夫,倡议应用 max-age 代替 Expires。

不过这样会产生一个问题,当文件更新了怎么办?怎么告诉浏览器从新申请文件?

能够通过更新页面中援用的资源链接地址,让浏览器被动放弃缓存,加载新资源。

具体做法是把资源地址 URL 的批改与文件内容关联起来,也就是说,只有文件内容变动,才会导致相应 URL 的变更,从而实现文件级别的准确缓存管制。什么货色与文件内容相干呢?咱们会很天然的联想到利用数据摘要要算法对文件求摘要信息,摘要信息与文件内容一一对应,就有了一种能够准确到单个文件粒度的缓存管制根据了。

参考资料:

  • webpack + express 实现文件准确缓存
  • webpack- 缓存
  • 张云龙 – 大公司里怎么开发和部署前端代码?

8\. 压缩文件

压缩文件能够缩小文件下载工夫,让用户体验性更好。

得益于 webpack 和 node 的倒退,当初压缩文件曾经十分不便了。

在 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())

9. 图片优化

(1). 图片提早加载

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

首先能够将图片这样设置,在页面不可见时图片不会加载:

<img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&u=7bd8f32788df6988833da6bd155c3cfbebc68006&v=4">

等页面可见时,应用 JS 加载图片:

const img = document.querySelector('img')
img.src = img.dataset.src

这样图片就加载进去了,残缺的代码能够看一下参考资料。

参考资料:

  • web 前端图片懒加载实现原理

(2). 响应式图片

响应式图片的长处是浏览器可能依据屏幕大小主动加载适合的图片。

通过 picture 实现

<picture>
    <source srcset="banner_w1000.jpg" media="(min-width: 801px)">
    <source srcset="banner_w800.jpg" media="(max-width: 800px)">
    <img src="banner_w800.jpg" alt="">
</picture>

通过 @media 实现

@media (min-width: 769px) {
    .bg {background-image: url(bg1080.jpg);
    }
}
@media (max-width: 768px) {
    .bg {background-image: url(bg768.jpg);
    }
}

(3). 调整图片大小

例如,你有一个 1920 * 1080 大小的图片,用缩略图的形式展现给用户,并且当用户鼠标悬停在下面时才展现全图。如果用户从未真正将鼠标悬停在缩略图上,则节约了下载图片的工夫。

所以,咱们能够用两张图片来履行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种方法,即对大图进行提早加载,在所有元素都加载实现后手动更改大图的 src 进行下载。

(4). 升高图片品质

例如 JPG 格局的图片,100% 的品质和 90% 品质的通常看不出来区别,尤其是用来当背景图的时候。我常常用 PS 切背景图时,将图片切成 JPG 格局,并且将它压缩到 60% 的品质,基本上看不出来区别。

压缩办法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。

以下附上 webpack 插件 image-webpack-loader 的用法。

npm i -D image-webpack-loader

webpack 配置

{test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000, /* 图片大小小于 1000 字节限度时会主动转成 base64 码援用 */
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    /* 对图片进行压缩 */
    {
      loader: 'image-webpack-loader',
      options: {bypassOnDebug: true,}
    }
  ]
}

(5). 尽可能利用 CSS3 成果代替图片

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

参考资料:

  • img 图片在 webpack 中应用

(6). 应用 webp 格局的图片

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

参考资料:

  • WebP 绝对于 PNG、JPG 有什么劣势?

10\. 通过 webpack 按需加载代码,提取第三库代码,缩小 ES6 转为 ES5 的冗余代码

懒加载或者按需加载,是一种很好的优化网页或利用的形式。这种形式实际上是先把你的代码在一些逻辑断点处罚来到,而后在一些代码块中实现某些操作后,立刻援用或行将援用另外一些新的代码块。这样放慢了利用的初始加载速度,加重了它的总体体积,因为某些代码块可能永远不会被加载。

依据文件内容生成文件名,联合 import 动静引入组件实现按需加载

通过配置 output 的 filename 属性能够实现这个需要。filename 属性的值选项中有一个 [contenthash],它将依据文件内容创立出惟一 hash。当文件内容发生变化时,[contenthash] 也会发生变化。

output: {filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
    path: path.resolve(__dirname, '../dist'),
},

提取第三方库

因为引入的第三方库个别都比较稳定,不会常常扭转。所以将它们独自提取进去,作为长期缓存是一个更好的抉择。这里须要应用 webpack4 的 splitChunk 插件 cacheGroups 选项。

optimization: {
      runtimeChunk: {name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个独自的 chunk。},
    splitChunks: {
        cacheGroups: {
            vendor: {
                name: 'chunk-vendors',
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                chunks: 'initial'
            },
            common: {
                name: 'chunk-common',
                minChunks: 2,
                priority: -20,
                chunks: 'initial',
                reuseExistingChunk: true
            }
        },
    }
},
  • test: 用于管制哪些模块被这个缓存组匹配到。一成不变传递进来的话,它默认会抉择所有的模块。能够传递的值类型:RegExp、String 和 Function;
  • priority:示意抽取权重,数字越大示意优先级越高。因为一个 module 可能会满足多个 cacheGroups 的条件,那么抽取到哪个就由权重最高的说了算;
  • reuseExistingChunk:示意是否应用已有的 chunk,如果为 true 则示意如果以后的 chunk 蕴含的模块曾经被抽取进来了,那么将不会从新生成新的。
  • minChunks(默认是 1):在宰割之前,这个代码块最小应该被援用的次数(译注:保障代码块复用性,默认配置的策略是不须要屡次援用也能够被宰割)
  • chunks (默认是 async):initial、async 和 all
  • name(打包的 chunks 的名字):字符串或者函数 (函数能够依据条件自定义名字)

缩小 ES6 转为 ES5 的冗余代码

Babel 转化后的代码想要实现和原来代码一样的性能须要借助一些帮忙函数,比方:

class Person {}

会被转换为:

"use strict";

function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");
  }
}

var Person = function Person() {_classCallCheck(this, Person);
};

这里 _classCallCheck 就是一个 helper 函数,如果在很多文件里都申明了类,那么就会产生很多个这样的 helper 函数。

这里的 @babel/runtime 包就申明了所有须要用到的帮忙函数,而 @babel/plugin-transform-runtime 的作用就是将所有须要 helper 函数的文件,从 @babel/runtime 包 引进来:

"use strict";

var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj};
}

var Person = function Person() {(0, _classCallCheck3.default)(this, Person);
};

这里就没有再编译出 helper 函数 classCallCheck 了,而是间接援用了 @babel/runtime 中的 helpers/classCallCheck

装置

npm i -D @babel/plugin-transform-runtime @babel/runtime

应用 .babelrc 文件中

"plugins": ["@babel/plugin-transform-runtime"]

参考资料:

  • Babel 7.1 介绍 transform-runtime polyfill env
  • 懒加载
  • Vue 路由懒加载
  • webpack 缓存
  • 一步一步的理解 webpack4 的 splitChunk 插件

11\. 缩小重绘重排

浏览器渲染过程

  1. 解析 HTML 生成 DOM 树。
  2. 解析 CSS 生成 CSSOM 规定树。
  3. 将 DOM 树与 CSSOM 规定树合并在一起生成渲染树。
  4. 遍历渲染树开始布局,计算每个节点的地位大小信息。
  5. 将渲染树每个节点绘制到屏幕。

重排

当扭转 DOM 元素地位或大小时,会导致浏览器从新生成渲染树,这个过程叫重排。

重绘

当从新生成渲染树后,就要将渲染树每个节点绘制到屏幕,这个过程叫重绘。不是所有的动作都会导致重排,例如扭转字体色彩,只会导致重绘。记住,重排会导致重绘,重绘不会导致重排。

重排和重绘这两个操作都是十分低廉的,因为 JavaScript 引擎线程与 GUI 渲染线程是互斥,它们同时只能一个在工作。

什么操作会导致重排?

  • 增加或删除可见的 DOM 元素
  • 元素地位扭转
  • 元素尺寸扭转
  • 内容扭转
  • 浏览器窗口尺寸扭转

如何缩小重排重绘?

  • 用 JavaScript 批改款式时,最好不要间接写款式,而是替换 class 来扭转款式。
  • 如果要对 DOM 元素执行一系列操作,能够将 DOM 元素脱离文档流,批改实现后,再将它带回文档。举荐应用暗藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个计划。

12\. 应用事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就能够治理某一类型的所有事件。所有用到按钮的事件(少数鼠标事件和键盘事件)都适宜采纳事件委托技术,应用事件委托能够节俭内存。

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

// 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)
  }
}) 

同时,我还从这位阿里大神手里薅到一份阿里内部资料。

有须要的点击这里收费支付材料 PDF

篇幅无限,仅展现局部内容

如果你须要这份完整版材料 pdf,【点击我】 就能够了。

心愿大家明年的金三银四面试顺利,拿下本人心仪的 offer!

正文完
 0