性能优化是一把双刃剑,有好有坏,好的一面是可以提升网站性能,坏的一面是配置复杂,或者规则太多,另外有些性能优化规则并不适用于所有场景,需要谨慎使用,读者应该以批判的眼光看待本文。
本文优化建议的参考资料将在每条建议之后或者文章末尾提供。
1.减少HTTP请求
一个完整的HTTP请求需要经过DNS查找、TCP握手、浏览器发送HTTP请求、服务器接收请求、服务器处理请求并返回响应、浏览器接收响应等过程。我们通过一个具体的例子来理解HTTP:
这是一个HTTP请求,文件大小为28.4KB。
术语解释:
排队:在请求队列中花费的时间。
Stalled:TCP 连接建立到实际可以传输数据之间的时间差,包括代理协商时间。
代理协商:与代理服务器协商所花费的时间。
DNS 查找:执行 DNS 查找所花费的时间。页面上的每个不同域都需要进行 DNS 查找。
初始连接/连接中:建立连接所花费的时间,包括 TCP 握手/重试和 SSL 协商。
SSL:完成 SSL 握手所花费的时间。
请求已发送:发送网络请求所花费的时间,通常为一毫秒。
等待(TFFB):TFFB 是从发出页面请求开始,直到收到响应数据的第一个字节的时间。
内容下载:接收响应数据所花费的时间。
从这个例子我们可以看出,实际数据下载时间只占13.05 / 204.16 = 6.39%总量的 1/3。文件越小,这个比例越小;文件越大,这个比例越高。这就是为什么建议将多个小文件合并为一个大文件,从而减少 HTTP 请求的数量。
参考:
理解资源时机
2. 使用 HTTP2
相对于HTTP1.1来说,HTTP2有几个优点:
解析速度更快
解析 HTTP1.1 请求时,服务器必须连续读取字节,直到遇到 CRLF 分隔符。解析 HTTP2 请求并不那么复杂,因为 HTTP2 是基于帧的协议,每个帧都有一个字段指示其长度。
多路复用
使用HTTP1.1时,如果要同时发出多个请求,则需要建立多个TCP连接,因为一个TCP连接一次只能处理一个HTTP1.1请求。
在 HTTP2 中,多个请求可以共享一个 TCP 连接,这称为多路复用。每个请求和响应都由一个流表示,并带有唯一的流 ID 来标识它。
多个请求和响应可以在 TCP 连接内无序发送,然后使用流 ID 在目的地重新组装。
报头压缩
HTTP2 提供了头压缩功能。
例如,考虑以下两个请求:
:authority: suoyinmulu.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.suoyinmulu.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: suoyinmulu.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.suoyinmulu.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 | 标题1 | 富 |
| 63 | 标题2 | 酒吧 |
| 64 | 标题3 | 蝙蝠 |
如果服务器收到请求,就会创建同样的表,
当客户端发送下一个请求时,如果头部信息相同,则直接发送一个头部块,如下图所示:
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 为例,简单描述一下 SSR 的流程。
客户端渲染流程
访问客户端呈现的网站。
服务器返回一个包含资源导入语句和的 HTML 文件
<div id="app"></div>。客户端通过HTTP向服务器请求资源,当必要的资源加载完成后才执行,
new Vue()实例化并渲染页面。
服务端渲染流程
访问服务器呈现的网站。
服务器检查当前路由组件需要哪些资源文件,然后将这些文件的内容填充到HTML文件中,如果有AJAX请求,则执行AJAX请求进行数据预取并填充到HTML文件中,最后返回本HTML页面。
当客户端接收到这个 HTML 页面后,它就可以立即开始渲染页面。与此同时,页面也会加载资源,当必要的资源加载完毕后,它就开始执行,
new Vue()实例化并接管页面。
从上面两个过程我们可以看出,区别在于第二步,客户端渲染的网站会直接返回HTML文件,而服务端渲染的网站会把页面渲染完整后再返回这个HTML文件。
这样做有什么好处?可以加快内容交付速度。
假设您的网站需要加载四个文件(a、b、c、d)才能完整呈现。每个文件的大小为 1 MB。
这样算下来,客户端渲染的网站需要加载4个文件和一个HTML文件才能完成首页渲染,共计4MB(忽略HTML文件大小)。而服务端渲染的网站只需要加载一个完全渲染好的HTML文件就能完成首页渲染,共计已经渲染好的HTML文件大小(一般不会太大,一般几百KB,我的个人博客网站(SSR)加载一个HTML文件就400KB)。这也是服务端渲染速度更快的原因。
参考:
vue-ssr-demo
Vue.js 服务器端渲染指南
4. 使用 CDN 托管静态资源
内容分发网络 (CDN) 是一组分布在多个地理位置的 Web 服务器。我们都知道,服务器距离用户越远,延迟就越高。CDN 旨在通过在多个位置部署服务器来解决此问题,使用户更接近服务器,从而缩短请求时间。
CDN 原理
当用户访问没有CDN的网站时,流程如下:
浏览器需要将域名解析为IP地址,因此会向本地DNS发出请求。
本地DNS依次向根服务器、顶级域名服务器、权威服务器发出请求,获取网站服务器的IP地址。
本地DNS将IP地址发送回浏览器,浏览器向网站服务器的IP地址发出请求并接收资源。
如果用户访问的是部署了CDN的网站,流程如下:
浏览器需要将域名解析为IP地址,因此会向本地DNS发出请求。
本地DNS依次向根服务器、顶级域名服务器、权威服务器发出请求,获取全局服务器负载均衡(GSLB)系统的IP地址。
然后本地 DNS 向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户所在位置,筛选出距离用户最近的本地负载均衡 (SLB) 系统,并将该 SLB 的 IP 地址返回给本地 DNS。
本地DNS将SLB的IP地址发送回浏览器,浏览器向SLB发出请求。
SLB根据浏览器请求的资源和地址,选择最优的缓存服务器并返回给浏览器。
然后浏览器根据SLB返回的地址重定向到缓存服务器。
如果缓存服务器有浏览器需要的资源,就把该资源发回给浏览器,如果没有,就向源服务器请求资源,发给浏览器,并在本地缓存。
参考:
什么是CDN?使用CDN有什么好处?
CDN 原理简化
5. 将 CSS 放在头部,将 JavaScript 文件放在底部
CSS 执行会阻止渲染并阻止 JS 执行
JS 加载和执行会阻塞 HTML 解析并阻止 CSSOM 构建
如果这些 CSS 和 JS 标签放在 HEAD 标签中,加载和解析时间较长,页面就会一片空白。因此,JS 文件应该放在最底层(不会阻塞 DOM 解析,但会阻塞渲染),以便在加载 JS 文件之前完成 HTML 解析,尽早将页面内容呈现给用户。
那么为什么CSS文件还要放在头部呢?
因为先加载 HTML 再加载 CSS 会让用户第一眼看到一个没有样式的“丑陋”页面。为了避免这种情况,应该将 CSS 文件放在 head 中。
另外JS文件也可以放在head中,只要script标签有defer属性,即异步下载,延迟执行。
参考:
使用 JavaScript 添加交互性
6. 使用字体图标(iconfont)代替图像图标
字体图标就是把图标做成字体,使用的时候就像字体一样,可以设置字体大小、颜色等属性,非常方便。而且字体图标是矢量图形,不会丢失清晰度,还有一个好处就是生成的文件特别小。
压缩字体文件
使用fontmin-webpack插件压缩字体文件(感谢Frontend Xiaowei提供)。
参考:
fontmin-webpack
Iconfont-阿里巴巴矢量图标库
7.充分利用缓存,避免重新加载相同的资源
为了避免用户每次访问网站都要请求文件,我们可以通过添加 Expires 或 max-age 来控制这种行为。Expires 设定一个时间,只要在此时间之前,浏览器就不会请求文件,而是直接使用缓存。Max-age 是一个相对时间,建议使用 max-age 而不是 Expires。
然而,这带来了一个问题:当文件更新时会发生什么?我们如何通知浏览器再次请求该文件?
这可以通过更新页面中引用的资源链接地址来实现,使得浏览器主动放弃缓存并加载新的资源。
具体做法是将资源地址的URL修改和文件内容关联起来,也就是说只有文件内容发生变化,对应的URL才会变化,从而实现文件级别的精准缓存控制。什么和文件内容关联呢?我们很自然地想到利用摘要算法,得到文件的摘要信息。摘要信息和文件内容一一对应,为精细到单个文件粒度的缓存控制提供了依据。
参考:
webpack + express 实现精准文件缓存
webpack 缓存
张云龙 - 大公司如何开发和部署前端代码?
8.压缩文件
压缩文件可以减少文件下载时间,提供更好的用户体验。
得益于webpack和node的发展,文件压缩现在变得非常方便。
在webpack中,可以使用以下插件进行压缩:
JavaScript:Uglify插件
CSS:MiniCssExtractPlugin
HTML:HtmlWebpack插件
其实,使用 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()],
}
节点配置
const compression = require('compression')
// Use before other middleware
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, /* Images smaller than 1000 bytes will be automatically converted to base64 code references */
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
/* Compress images */
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
(5)尽可能使用 CSS3 效果代替图像
许多图像可以用 CSS 效果(渐变、阴影等)绘制,在这些情况下,CSS3 效果更好。这是因为代码大小通常只是图像大小的几分之一甚至十分之一。
参考:
在 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' // Split webpack's runtime code into a separate 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:表示提取权重,数字越大表示优先级越高,由于一个模块可能满足多个cacheGroup的条件,所以按照权重最高的那个来提取;
repeatExistingChunk:是否使用现有的chunk,如果为true,则表示如果当前chunk包含已经提取的模块,则不会生成新的模块。
minChunks(默认为 1):拆分前此代码块应被引用的最小次数(注意:为确保代码块可重用性,默认策略不要求拆分多个引用)
块(默认为异步):initial、async 和 all
name(打包的chunk名称):字符串或者函数(函数可以根据条件自定义名称)
将 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 package:
"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不再被编译,而是helpers/classCallCheck从中引用@babel/runtime。
安装
npm i -D @babel/plugin-transform-runtime @babel/runtime
在.babelrc文件中使用
"plugins": [
"@babel/plugin-transform-runtime"
]
参考:
Babel 7.1 介绍 transform-runtime polyfill 环境
延迟加载
Vue 路由延迟加载
webpack 缓存
一步步理解webpack4的splitChunk插件
11.减少重流和重绘
浏览器渲染过程
解析 HTML 以生成 DOM 树。
解析CSS并生成CSSOM规则树。
结合DOM树和CSSOM规则树生成渲染树。
遍历渲染树开始布局,计算每个节点的位置和大小信息。
将渲染树的每个节点绘制到屏幕上。
回流焊
当DOM元素的位置或大小改变时,浏览器需要重新生成渲染树,这个过程称为重排。
重绘
重新生成渲染树后,渲染树的每个节点都需要绘制到屏幕上,这个过程称为重绘。并非所有操作都会引起重排,例如,更改字体颜色只会引起重绘。请记住,重排会引起重绘,但重绘不会引起重排。
重排和重绘操作都非常昂贵,因为 JavaScript 引擎线程和 GUI 渲染线程是互斥的,并且一次只能工作一个。
哪些操作会引起回流?
添加或删除可见的 DOM 元素
元素位置改变
元素大小改变
内容变更
浏览器窗口大小改变
如何减少回流和重绘?
使用JavaScript修改样式的时候,最好不要直接写样式,而是通过替换类来改变样式。
如果需要对某个 DOM 元素进行一系列操作,可以将该 DOM 元素从文档流中取出,进行修改后再放回文档中。建议使用隐藏元素(display:none)或文档片段(DocumentFragement),两者都可以很好地实现此方法。
12. 使用事件委托
事件委托利用事件冒泡,允许您指定单个事件处理程序来管理特定类型的所有事件。所有使用按钮的事件(大多数鼠标事件和键盘事件)都适合事件委托技术。使用事件委托可以节省内存。
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Pineapple</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)
}
})
13. 注意程序局部性
编写良好的计算机程序通常具有良好的局部性;它倾向于引用最近引用的数据项附近的数据项或最近引用的数据项本身。这种倾向被称为局部性原则。具有良好局部性的程序比局部性较差的程序运行得更快。
局部性通常有两种不同的形式:
时间局部性:在具有良好时间局部性的程序中,被引用过一次的内存位置很可能在不久的将来被引用多次。
空间局部性:在具有良好空间局部性的程序中,如果某个内存位置已经被引用过一次,则该程序很可能在不久的将来引用附近的内存位置。
时间局部性示例
function sum(arry) {
let i, sum = 0
let len = arry.length
for (i = 0; i < len; i++) {
sum += arry[i]
}
return sum
}
在这个例子中,变量 sum 在每次循环迭代中被引用一次,因此它具有良好的时间局部性。
空间局部性示例
具有良好空间局部性的程序
// Two-dimensional array
function sum1(arry, rows, cols) {
let i, j, sum = 0
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
sum += arry[i][j]
}
}
return sum
}
空间局部性较差的程序
// Two-dimensional array
function sum2(arry, rows, cols) {
let i, j, sum = 0
for (j = 0; j < cols; j++) {
for (i = 0; i < rows; i++) {
sum += arry[i][j]
}
}
return sum
}
再看上面两个空间局部性的例子,从每一行开始依次访问数组中每个元素的方法,如例子中所示,称为步幅为 1 的参考模式。
如果在数组中,每访问 k 个元素,则称为步幅为 k 的参考模式。
通常,随着步幅的增加,空间局部性会降低。
这两个例子有什么不同呢?不同之处在于,第一个例子按行扫描数组,扫描完一行后再转到下一行;第二个例子按列扫描数组,扫描一行中的一个元素后立即转到下一行的同一列元素。
数组按行顺序存储在内存中,因此逐行扫描数组的示例可获得步幅为 1 的参考模式,且具有良好的空间局部性;而另一个示例的步幅为行,但空间局部性极差。
性能测试
运行环境:
中央处理器: i5-7400
浏览器:Chrome 70.0.3538.110
对长度为9000的二维数组(子数组长度也是9000)测试空间局部性10次,取平均时间(毫秒),结果如下:
所用的例子是上面提到的两个空间局部性例子。
| 步伐 1 | 迈莱德 9000 |
|---|---|
| 124 | 2316 |
从上面的测试结果来看,步幅为1的数组的执行速度比步幅为9000的数组快一个数量级。
结论:
重复引用相同变量的程序具有良好的时间局部性
对于步幅为k的参考模式的程序,步幅越小,空间局部性越好;而步幅较大的程序在内存中跳来跳去,空间局部性就会很差
参考:
计算机系统:程序员的视角
14. if-else 与 switch
随着判断条件的增多,使用switch代替if-else变得越来越可取。
if (color == 'blue') {
} else if (color == 'yellow') {
} else if (color == 'white') {
} else if (color == 'black') {
} else if (color == 'green') {
} else if (color == 'orange') {
} else if (color == 'pink') {
}
switch (color) {
case 'blue':
break
case 'yellow':
break
case 'white':
break
case 'black':
break
case 'green':
break
case 'orange':
break
case 'pink':
break
}
像上面这种情况,从可读性角度考虑,使用 switch 比较好(JavaScript 的 switch 语句不是基于 hash 实现的,而是基于循环判断,所以从性能角度考虑,if-else 和 switch 是一样的)。
15. 查找表
当条件语句较多时,使用 switch 和 if-else 并不是最佳选择。在这种情况下,您可能需要尝试查找表。可以使用数组和对象构造查找表。
switch (index) {
case '0':
return result0
case '1':
return result1
case '2':
return result2
case '3':
return result3
case '4':
return result4
case '5':
return result5
case '6':
return result6
case '7':
return result7
case '8':
return result8
case '9':
return result9
case '10':
return result10
case '11':
return result11
}
此 switch 语句可以转换为查找表
const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]
return results[index]
如果条件语句不是数值而是字符串,则可以使用对象来构建查找表
const map = {
red: result0,
green: result1,
}
return map[color]
16.避免页面卡顿
60fps 和设备刷新率
目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果页面上有动画或渐变效果,或者用户正在滚动页面,浏览器需要以与设备屏幕刷新率匹配的速率渲染动画或页面。每
帧的预算时间刚好超过 16 毫秒(1 秒 / 60 = 16.66 毫秒)。但实际上,浏览器有内部工作要做,因此所有工作都需要在 10 毫秒内完成。如果不能满足这个预算,帧率就会下降,屏幕上的内容就会抖动。这种现象通常被称为卡顿,会对用户体验产生负面影响。
假设你用 JavaScript 修改 DOM,触发样式变化,经过 reflow 和 repaint,最后绘制到屏幕上。如果其中任何一个环节耗时过长,就会导致这一帧的渲染时间过长,平均帧率就会下降。假设这一帧耗时 50 毫秒,那么帧率就是 1s/50ms = 20fps,页面就会出现卡顿。
对于一些长时间运行的JavaScript,我们可以使用计时器来拆分和延迟执行。
for (let i = 0, len = arry.length; i < len; i++) {
process(arry[i])
}
假设上述循环结构由于 process() 的高复杂性或数组元素太多(或两者兼而有之)而花费的时间过长,您可能需要尝试拆分。
const todo = arry.concat()
setTimeout(function() {
process(todo.shift())
if (todo.length) {
setTimeout(arguments.callee, 25)
} else {
callback(arry)
}
}, 25)
如果你有兴趣了解更多,请查看《高性能 JavaScript》第 6 章和《高效前端:Web 高效编程与优化实践》第 3 章。
参考:
渲染性能
17.使用 requestAnimationFrame 实现视觉变化
从第 16 点可知,大多数设备的屏幕刷新率为 60 次/秒,也就是说每帧的平均时间为 16.66 毫秒。使用 JavaScript 实现动画效果时,最好的情况是代码在每一帧开始时开始执行。而确保 JavaScript 在一帧开始时运行的唯一方法是使用requestAnimationFrame。
/**
* If run as a requestAnimationFrame callback, this
* will be run at the start of the frame.
*/
function updateScreen(time) {
// Make visual updates here.
}
requestAnimationFrame(updateScreen);
如果您使用setTimeout或setInterval实现动画,回调函数将在帧中的某个时间点运行,可能就在最后,这通常会导致我们错过帧,从而导致卡顿。
参考:
优化 JavaScript 执行
18. 使用 Web Workers
Web Worker 使用其他工作线程独立于主线程运行。它们可以在不干扰用户界面的情况下执行任务。Worker 可以通过向创建它的 JavaScript 代码指定的事件处理程序发送消息来向该代码发送消息(反之亦然)。
Web Workers 适合处理与浏览器 UI 无关的纯数据或长时间运行的脚本。
创建一个新的 worker 很简单,只需要指定一个脚本 URI 来执行 worker 线程(main.js):
var myWorker = new Worker('worker.js');
// You can send messages to the worker through the postMessage() method and onmessage event
first.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}
second.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('Message posted to worker');
}
在worker中,收到消息后,我们可以编写一个事件处理函数代码作为响应(worker.js):
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
onmessage 处理函数在收到消息后立即执行,消息本身用作事件的数据属性。这里我们只需将两个数字相乘,然后再次使用 postMessage() 方法将结果发送回主线程。
回到主线程,我们再次使用onmessage来响应worker发回的消息:
myWorker.onmessage = function(e) {
result.textContent = e.data;
console.log('Message received from worker');
}
这里我们从消息事件中获取数据,并将其设置为result的textContent,这样用户就可以直接看到计算的结果。
请注意,在 worker 内部,您无法直接操作 DOM 节点,也无法使用 window 对象的默认方法和属性。不过,您可以在 window 对象下使用很多东西,包括数据存储机制,例如 WebSockets、IndexedDB 和 Firefox OS 特定的 Data Store API。
参考:
Web Workers
19. 使用按位运算
JavaScript 中的数字使用 IEEE-754 标准以 64 位格式存储。但在按位运算中,数字会转换为 32 位有符号格式。即使经过转换,按位运算也比其他数学和布尔运算快得多。
模数
由于偶数的最低位为0,奇数的最低位为1,因此模运算可以用按位运算代替。
if (value % 2) {
// Odd number
} else {
// Even number
}
// Bitwise operation
if (value & 1) {
// Odd number
} else {
// Even number
}
地面
~~10.12 // 10
~~10 // 10
~~'1.5' // 1
~~undefined // 0
~~null // 0
位掩码
const a = 1
const b = 2
const c = 4
const options = a | b | c
通过定义这些选项,您可以使用按位AND运算来确定a/b/c是否在选项中。
// Is option b in the options?
if (b & options) {
...
}
20. 不要覆盖本机方法
不管你的 JavaScript 代码优化到什么程度,都比不上原生方法。这是因为原生方法是用低级语言(C/C++)编写的,并被编译成机器码,成为浏览器的一部分。当原生方法可用时,尽量使用它们,尤其是数学运算和 DOM 操作。
21.降低 CSS 选择器的复杂性
(1).浏览器读取选择器的时候,遵循从右到左读取的原则。
让我们看一个例子
#block .text p {
color: red;
}
查找所有 P 元素。
检查结果 1 中找到的元素是否具有类名为“text”的父元素
检查结果 2 中找到的元素是否具有 ID 为“block”的父元素
(2)CSS选择器优先级
Inline > ID selector > Class selector > Tag selector
根据以上两条信息,我们可以得出结论。
选择器越短越好。
尝试使用高优先级的选择器,例如 ID 和类选择器。
避免使用通用选择器 *。
最后要说的是,根据我找到的资料,CSS 选择器没有必要优化,因为最慢和最快的选择器之间的性能差异非常小。
参考:
CSS 选择器性能
优化 CSS:ID 选择器和其他误区
22. 使用 Flexbox 代替早期的布局模型
在早期的 CSS 布局方法中,我们可以对元素进行绝对定位、相对定位或使用浮动。现在,我们有了一种新的布局方法flexbox,它比早期的布局方法有一个优势:性能更好。
下面的截图显示了在 1300 个盒子上使用浮动的布局成本:
然后我们使用 flexbox 重新创建这个示例:
现在,对于相同数量的元素和相同的视觉外观,布局时间要少得多(在本例中为 3.5 毫秒对 14 毫秒)。
但是,flexbox 兼容性仍然是一个问题,并非所有浏览器都支持它,因此请谨慎使用。
浏览器兼容性:
Chrome 29+
Firefox 28+
Internet Explorer 11
Opera 17+
Safari 6.1+(以 -webkit- 为前缀)
Android 4.4+
iOS 7.1+(以 -webkit- 为前缀)
参考:
使用 flexbox 代替早期的布局模型
23. 使用变换和不透明度属性实现动画
在 CSS 中,变换和不透明度属性的改变不会触发重排和重绘,它们是可以由合成器单独处理的属性。
参考:
使用变换和不透明度属性变化来实现动画
24.合理使用规则,避免过度优化
性能优化主要分为两类:
加载时间优化
运行时优化
以上23条建议,前10条属于加载时优化,后13条属于运行时优化。一般情况下,23条性能优化规则不需要全部套用,最好根据网站用户群体进行针对性调整,这样更省力省时。
在解决问题之前,首先需要确定问题是什么,否则就无从下手。所以在做性能优化之前,最好先调查一下网站的加载和运行性能。
检查加载性能
网站的加载性能主要取决于白屏时间和首屏时间。
白屏时间:从输入URL到页面开始显示内容的时间。
首屏时间:从输入URL到页面完全呈现的时间。
您可以在 之前放置以下脚本来获取白屏时间</head>。
<script>
new Date() - performance.timing.navigationStart
// You can also use domLoading and navigationStart
performance.timing.domLoading - performance.timing.navigationStart
</script>
new Date() - performance.timing.navigationStart您可以通过在事件中执行来获取首次显示时间window.onload。
检查运行时性能
通过Chrome的开发者工具,我们可以检查网站在运行时的性能。
打开网站,按F12选择性能,点击左上角的灰点,变成红色表示开始录制。此时可以模拟用户使用网站,录制完成后点击停止,即可看到网站运行时的性能报告。如果出现红色的块状,说明有掉帧现象;如果是绿色,说明FPS良好。性能的详细使用情况,请使用搜索引擎搜索,范围有限。
通过检查加载和运行时性能,我相信你已经对网站的性能有了大致的了解。所以你现在需要做的就是使用上面的 24 条建议来优化你的网站。行动起来吧!

