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

性能优化是把双刃剑,有好的一面也有坏的一面。好的一面就是能提高网站性能,坏的一面就是配置麻烦,或者要遵照的规则太多。而且某些性能优化规则并不适用全部场景,须要谨慎使用,请读者带着批判性的眼光来阅读本文。html

本文相关的优化建议的引用资料出处均会在建议后面给出,或者放在文末。前端

1. 减小 HTTP 请求

一个完整的 HTTP 请求须要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。接下来看一个具体的例子帮助理解 HTTP :vue

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

名词解释:webpack

  • 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 请求次数的缘由。git

参考资料:github

2. 使用 HTTP2

HTTP2 相比 HTTP1.1 有以下几个优势:web

解析速度快

服务器解析 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 协议。

参考资料:

3. 使用服务端渲染

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

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

  • 优势:首屏渲染快,SEO 好。
  • 缺点:配置麻烦,增长了服务器的计算压力。

参考资料:

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. 若是缓存服务器有浏览器须要的资源,就将资源发回给浏览器。若是没有,就向源服务器请求资源,再发给浏览器并缓存在本地。

参考资料:

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 插件对字体文件进行压缩(感谢前端小伟提供)。

参考资料:

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

为了不用户每次访问网站都得请求文件,咱们能够经过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间以前,浏览器都不会请求文件,而是直接使用缓存。而 max-age 是一个相对时间,建议使用 max-age 代替 Expires 。

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

能够经过更新页面中引用的资源连接地址,让浏览器主动放弃缓存,加载新资源。

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

参考资料:

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

这样图片就加载出来了,完整的代码能够看一下参考资料。

参考资料:

(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 效果更好。由于代码大小一般是图片大小的几分之一甚至几十分之一。

参考资料:

(6). 使用 webp 格式的图片

WebP 的优点体如今它具备更优的图像数据压缩算法,能带来更小的图片体积,并且拥有肉眼识别无差别的图像质量;同时具有了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都至关优秀、稳定和统一。

参考资料:

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"
]

参考资料:

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!