web性能优化宝典

web性能优化宝典

Why Is Website Performance Important?

Research shows that the amount of time a user will wait before losing focus is roughly from 0. to 3 seconds. If your website takes longer than that to display important information to the user, the user will lose focus and possibly close the browser window.

1 静态资源

通过优化和压缩网页资源来最大限度地减少总下载大小,是提高网页加载速度最有效的措施。

1.1 缩减文件大小

1.1.1 JavaScript

借助工具 webpack 以及 terser-webpack-plugin 插件来实现对 JS 文件的压缩和混淆,删除 JavaScript 代码中所有注释、换行符号及无用的空格,从而压缩 JS 文件大小,优化页面加载速度。

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
test: /\.js(\?.*)?$/i,
extractComments: true,
parallel: 4, // 启用多进程来运行并且设置了并发运行的次数为4
terserOptions: {
ecma: undefined,
parse: {},
compress: {
// 移除日志
drop_console: true,
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}, // 压缩配置
mangle: true, // Note `mangle.properties` is `false` by default.
module: false,
// Deprecated
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
format: null,
toplevel: true,
nameCache: null,
ie8: false, // 是否支持 IE8
keep_classnames: undefined,
keep_fnames: false,
safari10: true,
},
}),
],
},
};

1.1.2 CSS

使用 mini-css-extract-plugin 该插件将 CSS 提取到单独的文件中。它为每个包含 CSS 的 JS 文件创建一个 CSS 文件。使用 optimize-css-assets-webpack-plugin 插件进行 CSS 文件压缩。

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var OptimizeCssAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
module: {
rules: [
{
test: /\.css$/,
loader: MiniCssExtractPlugin.extract("style-loader", "css-loader"),
loader: MiniCssExtractPlugin.loader,
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: "url-loader",
options: {
limit: 10, // 小于10KB图片,转base64编码
},
},
],
},
plugins: [
new ExtractTextPlugin("styles.css"),
//new OptimizeCssAssetsPlugin()
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require("cssnano"),
cssProcessorPluginOptions: {
preset: ["default", { discardComments: { removeAll: true } }],
},
canPrint: true,
}),
],
};

1.1.3 HTML

使用 html-webpack-plugin 插件的 minify 配置项来完成对 HTML 文件的压缩。

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
plugins: [
new HtmlWebpackPlugin({
// 把生成的 index.html 文件的内容的没用空格去掉,减少空间
minify: {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
minifyCSS: true,
},
template: "src/index.html",
}),
];
}

1.2 图片优化

1.2.1 延迟加载(懒加载)

页面初始加载时,先不给图片设置路径,只有当图片出现在浏览器可视区域时,才去加载真正的图片,这就是延迟加载。
实现思路:

  1. 默认给图片设置一个自定义属性(data-src);
  2. 页面加载完成后,根据 scrollTop 判断图片是否在用户的视野内,如果在,则将 data-src 属性中的值取出存放到 src 属性中;
  3. 在滚动事件中重复判断图片是否进入浏览器可视区域(注意使用节流函数);如果进入,则将 data-src 属性中的值取出存放到 src 属性中。

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Lazyload</title>
<style>
img {
display: block;
margin-bottom: 50px;
height: 200px;
width: 400px;
}
</style>
</head>
<body>
<img class="lazy" data-src="./img/default.png" data-src="./img/1.jpg" />
<img class="lazy" data-src="./img/default.png" data-src="./img/2.jpg" />
<img src="./img/default.png" data-src="./img/3.jpg" />
<img src="./img/default.png" data-src="./img/4.jpg" />
<img src="./img/default.png" data-src="./img/5.jpg" />
<img src="./img/default.png" data-src="./img/6.jpg" />
<img src="./img/default.png" data-src="./img/7.jpg" />
<img src="./img/default.png" data-src="./img/8.jpg" />
<img src="./img/default.png" data-src="./img/9.jpg" />
<img src="./img/default.png" data-src="./img/10.jpg" />
</body>
</html>

先获取所有图片的 dom,通过 document.body.clientHeight 获取可视区高度,再使用 element.getBoundingClientRect() API 直接得到元素相对浏览的 top 值, 遍历每个图片判断当前图片是否到了可视区范围内。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function lazyload() {
let viewHeight = document.body.clientHeight; //获取可视区高度
let imgs = document.querySelectorAll("img[data-src]");
imgs.forEach((item, index) => {
if (item.dataset.src === "") return;

// 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
let rect = item.getBoundingClientRect();
if (rect.bottom >= 0 && rect.top < viewHeight) {
item.src = item.dataset.src;
item.removeAttribute("data-src");
}
});
}

最后给 window 绑定 onscroll 事件。

window.addEventListener('scroll', throttle(lazyload, 200))

IntersectionObserver:
IntersectionObserver 是一个新的 API,可以自动”观察”元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做”交叉观察器”。我们来看一下它的用法:

1
2
3
4
5
6
7
8
9
10
var io = new IntersectionObserver(callback, option);

// 开始观察
io.observe(document.getElementById("example"));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数:callback 是可见性变化时的回调函数,option 是配置对象(该参数可选)。

目标元素的可见性变化时,就会调用观察器的回调函数 callback。callback 一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

1
2
3
var io = new IntersectionObserver((entries) => {
console.log(entries);
});

callback 函数的参数(entries)是一个数组,每个成员都是一个 IntersectionObserverEntry 对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries 数组就会有两个成员。

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒;
  • target:被观察的目标元素,是一个 DOM 节点对象;
  • isIntersecting: 目标是否可见;
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回 null;
  • boundingClientRect:目标元素的矩形区域的信息;
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息;
  • intersectionRatio:目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为 1,完全不可见时小于等于 0。

用 IntersectionObserver 实现图片懒加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const imgs = document.querySelectorAll("img[data-src]");

let observer = new IntersectionObserver((entries, self) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let img = entry.target;
let src = img.dataset.src;
if (src) {
img.src = src;
img.removeAttribute("data-src");
}
// 解除观察
self.unobserve(entry.target);
}
});
});

imgs.forEach((image) => {
observer.observe(image);
});

1.2.2 合理的尺寸

1.2.2.1 实际大小

例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。
所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。

1.2.2.2 响应式图片(媒体查询)

响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。
通过 picture 实现

1
2
3
4
5
<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 实现

1
2
3
4
5
6
7
8
9
10
@media (min-width: 769px) {
.bg {
background-image: url(bg1080.jpg);
}
}
@media (max-width: 768px) {
.bg {
background-image: url(bg768.jpg);
}
}

1.2.3 图片压缩

例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。用 PS 切背景图时,将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。
压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。
webpack 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
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,
}
}
]
}

1.2.4 尽可能利用 CSS3 效果代替图片

有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。

1.2.5 使用 webp 格式的图片

WebP 的优势体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性,在 JPEG 和 PNG 上的转化效果都相当优秀。

1.3 静态资源使用 CDN

内容分发网络(CDN)是一组分布在多个不同地理位置的 Web 服务器。我们都知道,当服务器离用户越远时,延迟越高。CDN 就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。

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

2 页面

2.1 减少 HTTP 请求

每个请求都是有成本的,既包含时间成本也包含资源成本。一个完整的请求都需要经过 DNS 寻址、与服务器建立连接、发送数据、等待服务器响应、接收数据这样一个 “漫长” 而复杂的过程。时间成本就是用户需等待这个资源加载结束;资源成本就是由于每个请求都需要携带数据,因此每个请求都需要占用带宽。另外,由于浏览器进行并发请求的请求数是有上限的,因此请求数多了以后,浏览器需要分批进行请求,因此会增加用户的等待时间,会给用户造成站点速度慢这样一个印象,即使可能用户能看到的第一屏的资源都已经请求完了,但是浏览器的进度条会一直存在。

2.1.1 从设计实现层面简化页面

如果页面像百度首页一样简单,那么接下来的规则基本上都用不着了。保持页面简洁、减少资源的使用是最直接有效的。

2.1.2 减少请求文件的个数

通过工具将多个零散的 JS、CSS 文件压缩合并后使用。

2.1.3 将图片转为 Base64 编码

采用 Base64 编码方式将图片直接嵌入到网页中,而不是从外部载入。

2.1.4 其他

  • 避免无效请求(图片,JavaScript,CSS 等 404);
  • 避免页面跳转或者重定向;
  • 使用字体图标 iconfont 代替图片图标;

2.2 延迟加载脚本

<script> 标签上有两个属性可以为我们提供延迟加载脚本:defer 和 async。

2.2.1 defer

defer 属性会告诉浏览器不用等待这个脚本加载,浏览器则可以继续处理 HTML 构建 DOM 元素,当 DOM 元素完全构建完成后,才会加载脚本。

1
2
3
4
5
6
<p>...content before script...</p>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- visible immediately -->
<p>...content after script...</p>

总结:

  1. 带有 defer 属性的 <script> 标签永远不会阻塞页面;
  2. 延迟加载的脚本始终在 DOM 准备就绪时执行(但在 DOMContentLoaded 事件之前)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<p>...content before scripts...</p>

<script>
document.addEventListener("DOMContentLoaded", () =>
alert("DOM ready after defer!")
);
</script>

<script
defer
src="https://javascript.info/article/script-async-defer/long.js?speed=1"
></script>

<p>...content after scripts...</p>

同样都是延迟脚本时,执行会保持相对顺序。

1
2
3
4
5
6
7
8
<script
defer
src="https://javascript.info/article/script-async-defer/long.js"
></script>
<script
defer
src="https://javascript.info/article/script-async-defer/small.js"
></script>

像上边两个 JS 文件都拥有 defer 属性,即使 small.js 可能先加载完毕,依旧会等待 long.js 加载完执行后在执行。js 之前有相互依赖关系时,这一点就需要注意。

注意:

  • defer 属性仅适用于外部脚本;
  • 如果 <script> 标记没有 src,则将忽略 defer 属性。

2.2.2 async

async 属性有点像 defer。这也使脚本无阻塞。但是它在行为上有着重要的区别;
如果有 async 属性,则意味着这些脚本之间是完全独立的。

  1. 浏览器也不会阻塞带有 async 属性的脚本和 defer 一致;
  2. 其他脚本不会等待带有 async 属性的脚本,带有 async 属性的脚本也不会等待其他脚本;
  3. DOMContentLoaded 和 带有 async 属性的脚本也不会互相等待;
  4. 带有 async 属性的脚本以“加载优先”的原则顺序运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<p>...content before scripts...</p>

<script>
document.addEventListener("DOMContentLoaded", () => alert("DOM ready!"));
</script>

<script
async
src="https://javascript.info/article/script-async-defer/long.js"
></script>
<script
async
src="https://javascript.info/article/script-async-defer/small.js"
></script>

<p>...content after scripts...</p>

2.3 删除重复的脚本

团队中形成良好工程目录结构,按照工程结构规范去存放脚本,团队成员之前提前沟通好,避免出现重复脚本。例如在实际开发过程中不同的功能模块中团队成员都各自引入同一个脚本库,导致脚本重复问题。

2.4 减少 DOM 的数量

  • 布局是否合理,是否是更具语义化的标签;
  • 通过伪元素实现对应的功能;
  • 展示大数据量时,是否有分页展示;
  • dom 元素是否按需加载。

可使用 document.getElementsByTagName('*').length; 检查页面元素数量。

2.5 样式表放页面顶部

将 CSS 文件放在 <head> 标签中。

思考:为何将样式表放到页面头部?

2.5.1 浏览器渲染页面流程

  1. 浏览器首先使用 HTTP 协议或者 HTTPS 协议,向服务端请求页面;
  2. 把请求回来的 HTML 代码经过解析,构建成 DOM 树;
  3. 计算 DOM 树上的 CSS 属性;
  4. 最后根据 CSS 属性对元素逐个进行渲染,得到内存中的位图;
  5. 一个可选的步骤是对位图进行合成,这会极大地增加后续绘制的速度;
  6. 合成之后,再绘制到界面上。

在上边 2、3 步时,构建 DOM 的过程是:从父到子,从先到后,一个一个节点构造,并且挂载到 DOM 树上的,在这个过程中,我们依次拿到上一步构造好的元素,去检查它匹配到了哪些规则,再根据规则的优先级,做覆盖和调整。整个过程中构建 DOM 树和匹配 CSS 规则是同步进行的。

2.5.2 重排(reflow)

当改变 DOM 元素位置或大小时,会导致浏览器重新生成渲染树,这个过程叫重排。重排的示例包括:添加或删除内容,显式或隐式更改宽度,高度,字体,字体大小等。重排对于性能影响更严重,因为重排导致部分页面甚至整个页面布局的更改。

2.5.3 重绘(repaint)

当元素的外观发生了明显更改但不影响其布局更改时,就会发生重绘。例如,轮廓,可见性,背景或颜色。根据 Opera 的说法,重绘是很耗性能的,因为浏览器必须验证 DOM 树中所有其他节点的可见性。

其他 CSS 优化策略

  • 使用简写,使用缩写语句,如下面所示的 margin 声明,可以从根本上减小 CSS 文件的大小
1
2
3
4
5
6
7
8
9
10
p {
margin-top: 1px;
margin-right: 2px;
margin-bottom: 3px;
margin-left: 4px;
}

p {
margin: 1px 2px 3px 4px;
}
  • 使用颜色快捷方式
1
2
3
4
5
6
target {
background-color: #ffffff;
}
target {
background: #fff;
}
  • 删除不必要的零和单位,CSS 支持多种单位和数字格式。它们是一个值得感谢的优化目标——可以删除尾随和跟随的零,如下面的代码片段所示。此外,请记住,零始终是零,添加维度不会为包含的信息附带价值。
1
2
3
4
5
6
padding: 0.2em;
margin: 20em;
avalue: 0px;
padding: 0.2em;
margin: 20em;
avalue: 0;
  • 省略 px,提高性能的一个简单方法是使用 CSS 标准的一个特性。为 0 的数值默认单位是 px—— 删除 px 可以为每个数字节省两个字节。
1
2
3
4
5
6
7
8
9
h2 {
padding: 0px;
margin: 0px;
}

h2 {
padding: 0;
margin: 0;
}
  • 删除空格,空格——考虑制表符、回车符和空格——使代码更容易阅读,但从解析器的角度看,它没有什么用处。在发布前删除它们,更好的方法是将此任务委托给 shell 脚本或类似的工具。
  • 删除注释,注释对编译器也没有任何作用。创建一个自定义解析器,以便在发布之前删除它们。这不仅节省了带宽,而且还确保攻击者和克隆者更难理解手头代码背后的思想(针对线上)。
  • 避免需要性能要求的属性,分析表明,一些标签比其他标签更消耗性能。以下这些解析会影响性能—如果在没有必要的情况,尽量不要使用它们。
1
2
3
4
5
6
border-radius
box-shadow
transform
filter
:nth-child
position: fixed;

2.6 缓存 HTTP 请求

2.6.1 为何要使用缓存

  • 缓存可以减少用户等待时间,提升用户体验,直接从内存或磁盘中取缓存数据肯定是比从服务器请求更快的;
  • 减少网络带宽消耗:对于网站运营者和用户,带宽都代表着成本,过多的带宽消耗,都需要支付额外的费用。试想一下如果可以使用缓存,只会产生极小的网络流量,这将有效的降低运营成本;
  • 降低服务器压力:给网络资源设定有效期之后,用户可以重复使用本地的缓存,减少对源服务器的请求,降低服务器的压力。

2.6.2 缓存读写规则

当浏览器对一个资源(比如一个外链的 a.js)进行请求的时候会发生什么?从缓存的角度来看大概如下:

  1. 调用 Service Worker 的 fetch 事件获取资源;
  2. 查看 memory cache;
  3. 查看 disk cache;
    这里又细分: 1)如果有强制缓存且未失效,则使用强制缓存,不请求服务器。这时的状态码全部是 200; 2)如果有强制缓存但已失效,使用协商缓存,比较后确定 304 还是 200。
  4. 发送网络请求,等待网络响应;
  5. 把响应内容存入 disk cache (如果请求头信息配置可以存的话);
  6. 把响应内容的引用存入 memory cache (无视请求头信息的配置,除了 no-store 之外);
  7. 把响应内容存入 Service Worker 的 Cache Storage (如果 Service Worker 的脚本调用了 cache.put())。

2.6.3 缓存位置

从浏览器开发者工具的 Network 面板下某个请求的 Size 中可以看到当前请求资源的大小以及来源,从这些来源我们就知道该资源到底是从 memory cache 中读取的呢,还是从 disk cache 中读取的,亦或者是服务器返回的。而这些就是缓存位置:

2.6.3.1 Service Worker

是一个注册在指定源和路径下的事件驱动 worker;特点是:

  • 运行在 worker 上下文,因此它不能访问 DOM;
  • 独立于主线程之外,不会造成阻塞;
  • 设计完全异步,所以同步 API(如 XHR 和 localStorage )不能在 Service Worker 中使用;
  • 最后处于安全考虑,必须在 HTTPS 环境下才可以使用。

说了这么多特点,那它和缓存有啥关系?其实它有一个功能就是离线缓存:Service Worker Cache;区别于浏览器内部的 memory cache 和 disk cache,它允许我们自己去操控缓存,具体操作过程可以查阅 Using_Service_Workers;通过 Service Worker 设置的缓存会出现在浏览器开发者工具 Application 面板下的 Cache Storage 中。

2.6.3.2 memory cache

是浏览器内存中的缓存,相比于 disk cache 它的特点是读取速度快,但容量小,且时效性短,一旦浏览器 tab 页关闭,memory cache 就将被清空。memory cache 会自动缓存所有资源吗?答案肯定是否定的,当 HTTP 头设置了 Cache-Control: no-store 的时候或者浏览器设置了 Disabled cache 就无法把资源存入内存了,其实也无法存入硬盘。当从 memory cache 中查找缓存的时候,不仅仅会去匹配资源的 URL,还会看其 Content-type 是否相同。

2.6.3.3 disk cache

也叫 HTTP cache 是存在硬盘中的缓存,根据 HTTP 头部的各类字段进行判定资源的缓存规则,比如是否可以缓存,什么时候过期,过期之后需要重新发起请求吗?相比于 memory cache 的 disk cache 拥有存储空间时间长等优点,网站中的绝大多数资源都是存在 disk cache 中的。

  • 浏览器如何判断一个资源是存入内存还是硬盘呢?关于这个问题,网上说法不一,不过比较靠谱的观点是:对于大文件大概率会存入硬盘中;当前系统内存使用率高的话,文件优先存入硬盘。

2.6.4 缓存策略

根据 HTTP header 的字段又可以将缓存分成强缓存和协商缓存。强缓存可以直接从缓存中读取资源返回给浏览器而不需要向服务器发送请求,而协商缓存是当强缓存失效后(过了过期时间),浏览器需要携带缓存标识向服务器发送请求,服务器根据缓存标识决定是否使用缓存的过程。强缓存的字段有:Expires 和 Cache-Control。协商缓存的字段有:Last-Modified 和 ETag。

2.6.4.1 强制缓存

从上图我们得知,强制缓存,在缓存数据未失效的情况下,可以直接使用缓存数据,那么浏览器是如何判断缓存数据是否失效呢?
我们知道,在没有缓存数据的时候,浏览器向服务器请求数据时,服务器会将数据和缓存规则一并返回,缓存规则信息包含在响应 header 中。

对于强制缓存来说,响应 header 中会有两个字段来标明失效规则(Expires/Cache-Control)
使用 chrome 的开发者工具,可以很明显的看到对于强制缓存生效时,网络请求的情况

Expires

Expires 是 HTTP/1.0 的字段,表示缓存过期时间,它是一个 GMT 格式的时间字符串。Expires 需要在服务端配置(具体配置也根据服务器而定),浏览器会根据该过期日期与客户端时间对比,如果过期时间还没到,则会去缓存中读取该资源,如果已经到期了,则浏览器判断为该资源已经不新鲜要重新从服务端获取。由于 Expires 是一个绝对的时间,所以会局限于客户端时间的准确性,从而可能会出现浏览器判断缓存失效的问题。如下是一个 Expires 示例,是一个日期/时间:

Expires: Wed, 21 Oct 2020 07:28:00 GMT

Cache-Control

它是 HTTP/1.1 的字段,其中的包含的值很多:

  • max-age 最大缓存时间,值的单位是秒,在该时间内,浏览器不需要向浏览器请求。这个设置解决了 Expires 中由于客户端系统时间不准确而导致缓存失效的问题;
  • must-revalidate:如果超过了 max-age 的时间,浏览器必须向服务器发送请求,验证资源是否还有效;
  • public 响应可以被任何对象(客户端、代理服务器等)缓存;
  • private 响应只能被客户端缓存;
  • no-cache 跳过强缓存,直接进入协商缓存阶段;
  • no-store 不缓存任何内容,设置了这个后资源也不会被缓存到内存和硬盘。

Cache-Control 的值是可以混合使用的,比如:

Cache-Control: private, max-age=0, no-cache

当混合使用的时候它们的优先级如下图所示:

「当 Expires 和 Cache-Control 都被设置的时候,浏览器会优先考虑后者」。当强缓存失效的时候,则会进入到协商缓存阶段。具体细节是这样:浏览器从本地查找强缓存,发现失效了,然后会拿着缓存标识请求服务器,服务器拿着这个缓存标识和对应的字段进行校验资源是否被修改,如果没有被修改则此时响应状态会是 304,且不会返回请求资源,而是直接从浏览器缓存中读取。

2.6.4.2 协商缓存

浏览器缓存标识可以是:Last-Modified 和 ETag。

Last-Modified

资源的最后修改时间;

  • 第一次请求的时候,响应头会返回该字段告知浏览器资源的最后一次修改时间;
  • 浏览器会将值和资源存在缓存中;
  • 再次请求该资源的时候,如果强缓存过期,则浏览器会设置请求头的 If-Modifined-Since 字段值为存储在缓存中的上次响应头 Last-Modified 的值,并且发送请求;
  • 服务器拿着 If-Modifined-Since 的值和 Last-Modified 进行对比。如果相等,表示资源未修改,响应 304;如果不相等,表示资源被修改,响应 200,且返回请求资源。如果资源更新的速度是小于 1 秒的,那么该字段将失效,因为 Last-Modified 时间是精确到秒的。所以有了 ETag。

ETag

根据资源内容生成的唯一标识,资源是否被修改的判断过程和上面的一致,只是对应的字段替换了。Last-Modified 替换成 ETag,If-Modifined-Since 替换成 If-None-Match。

当 Last-Modified 和 ETag 都被设置的时候,浏览器会优先考虑后者。

总结:

对于强制缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在时间内,执行比较缓存策略。
对于比较缓存,将缓存信息中的 Etag 和 Last-Modified 通过请求发送给服务器,由服务器校验,返回 304 状态码时,浏览器直接使用缓存。

浏览器第一次请求:

浏览器再次请求时:

2.6.5 浏览器的行为

浏览器地址栏输入 URL 后回车:查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。

  • 普通刷新 (F5):因为 TAB 页并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话),其次才是 disk cache。
  • 强制刷新 (ctrl+F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache)。服务器直接返回 200 和最新内容。
  • 当在开发者工具 Network 面板下设置了 Disabled cache 禁用缓存后,浏览器将不会从 memory cache 或者 disk cache 中读取缓存,而是直接发起网络请求。

2.6.6 前端开发设置不缓存

在引用 js、css 文件的 url 后边加上 ?+Math.random()

<script src="/js/test.js?+Math.random()"></script>

设置 html 页面不让浏览器缓存的方法。

1
2
3
<meta http-equiv="pragma" content="no-cache" />
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
<meta http-equiv="expires" content="Wed, 26 Feb 1997 00:00:00 GMT" />

2.7 预加载关键请求

可以使用不同的预取和预加载技术向浏览器通知在页面展示时需要哪些资源。

2.7.1 Preload

当浏览器解析到 preload 会立即进行资源的请求,需要注意的是使用 preload 进行预加载时需要指定文件的类型。

<link href=/js/chunk-vendors.5e63c7cf.js rel=preload as=script>

2.7.2 DNS-Prefetch

DNS 预取。可以告诉浏览器,哪些域名需要提前解析为 IP 地址。当浏览网页时,浏览器会在加载网页时对网页中的域名进行解析缓存,这样在单击当前网页中的连接时就无需进行 DNS 的解析,减少用户等待时间,提高用户体验。

<link rel="dns-prefetch" href="//sematext.com">

2.7.3 Prefetch

预取。如果确定将来需要特定资源,则可以要求浏览器请求该项目并将其存储在缓存中以供以后使用。当浏览器解析到 prefetch 时,不会立即请求资源,会等待浏览器空闲以后再进行资源的请求

<link rel="prefetch" href="logo.png">

2.7.4 Preconnect

TCP 预连接。与 DNS 预取方法非常相似,预连接将解析 DNS,但也会进行 TCP 握手和 TLS 协商(可选)。

<link rel="preconnect" href="https://www.sematext.com">

3 代码

3.1 Web Worker

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

3.2 基本用法

3.2.1 主线程

const worker = new Worker('work.js')

Worker()构造函数的参数是一个脚本文件,该文件就是 Worker 线程所要执行的任务。由于 Worker 不能读取本地文件,所以这个脚本必须来自网络。如果下载没有成功(比如 404 错误),Worker 就会默默地失败。

Worker()构造函数的参数也可以是一个与主线程在同一个网页的代码或者一个由二进制对象创建的 URL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message',function(){
postMessage('some message');
},false);
</script>
</body>
</html>

//worker代码字符串
const funStr = '···'

//通过代码字符串直接创建二进制对象
const blob1 = new Blob([funStr]);

//通过script标签内容来创建二进制对象
var blob2 = new Blob([document.querySelector('#worker').textContent]);

var url1 = window.URL.createObjectURL(blob1);
// 通过二进制对象创建worker
var worker =new Worker(url);
// 通过js文件的网络地址创建worker
var worker2 =new Worker('worker.js');
worker.onmessage = function(e){
// e.data === 'some message'
};

在 web 领域,Blob 对象表示一个只读原始数据的类文件对象,虽然是二进制原始数据但是是类似文件的对象,因此可以像操作文件对象一样操作 Blob 对象。

然后,主线程调用 worker.postMessage()方法,向 Worker 发消息。

1
2
worker.postMessage("Hello World");
worker.postMessage({ method: "echo", args: ["Work"] });

worker.postMessage()方法的参数,就是主线程传给 Worker 的数据。它可以是各种数据类型,包括二进制数据。接着,主线程通过 worker.onmessage 指定监听函数,接收子线程发回来的消息。

1
2
3
4
5
6
7
8
worker.onmessage = function (event) {
console.log("Received message " + event.data);
doSomething();
};
function doSomething() {
// 执行任务
worker.postMessage("Work done!");
}

Worker 完成任务以后,主线程就可以把它关掉。

worker.terminate();

3.2.2 Worker 线程

worker 线程内部需要有一个监听函数,监听 message 事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//写法一
self.addEventListener(
"message",
function (e) {
self.postMessage("You said: " + e.data);
},
false
);

//写法二
this.addEventListener(
"message",
function (e) {
self.postMessage("You said: " + e.data);
},
false
);

//写法三
addEventListener(
"message",
function (e) {
self.postMessage("You said: " + e.data);
},
false
);

//写法四
onmessage = function (e) {
postMessage("You said: " + e.data);
};

向主线程发消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
self.addEventListener(
"message",
function (e) {
var data = e.data;
switch (data.cmd) {
case "start":
s;
elf.postMessage("WORKER STARTED: " + data.msg);
break;
case "stop":
self.postMessage("WORKER STOPPED: " + data.msg);
self.close();
// Terminates the worker. break;
default:
self.postMessage("Unknown command: " + data.msg);
}
},
false
);

self.close()用于在 Worker 内部关闭自身。
terminate 是 worker 创建者自己销毁 worker,close 是 worker 自己销毁自己。

3.2.3 错误处理

主线程可以监听 Worker 是否发生错误。如果发生错误,Worker 会触发主线程的 error 事件。

1
2
3
4
5
6
7
8
9
worker.onerror(function (event) {
console.log(
["ERROR: Line ", e.lineno, " in ", e.filename, ": ", e.message].join("")
);
});
// 或者
worker.addEventListener("error", function (event) {
// ...
});

3.2.4 关闭 Worker

使用完毕,为了节省系统资源,必须关闭 Worker。

1
2
3
4
5
// 主线程
worker.terminate();

// Worker 线程
self.close();

3.3 注意事项

  • 同源限制:分配给 Worker 线程运行的脚本文件,必须与主线程的脚步问文件同源;
  • DOM 限制:Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在页面的 DOM 对象,也无法使用 document、window、parent 这些对象,但是,worker 线程可以使用 navigator 和 location 对象;
  • 通信联系:Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成;
  • 脚本限制:Worker 线程不能执行 alert()方法和 confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求;
  • 文件限制:创建 Worker 线程时无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

3.4 防抖和节流

在前端开发的过程中,我们经常会需要绑定一些持续触发的事件,如 resize、scroll、mousemove 等等,但有些时候我们并不希望在事件持续触发的过程中那么频繁地去执行函数。

通常这种情况下我们怎么去解决的呢?一般来讲,防抖和节流是比较好的解决方案。

让我们先来看看在事件持续触发的过程中频繁执行函数是怎样的一种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
<div
id="content"
style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"
></div>
<script>
let num = 1;
let content = document.getElementById("content");

function count() {
content.innerHTML = num++;
}
content.onmousemove = count;
</script>

在上述代码中,div 元素绑定了 mousemove 事件,当鼠标在 div(灰色)区域中移动的时候会持续地去触发该事件导致频繁执行函数。效果如下

可以看到,在没有通过其它操作的情况下,函数被频繁地执行导致页面上数据变化特别快。所以,接下来让我们来看看防抖和节流是如何去解决这个问题的。

3.4.1 防抖函数(debounce)

所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间,即会重新开始等待,直到过了 n 秒后,才会再次执行。通俗理解就是首次事件触发并执行函数之后的 n 秒内,如果事件一直在触发,那么函数将永远不会执行,直到事件不触发了之后,并等待 n 秒之后,再次触发事件才会执行函数。

完整的防抖函数:

  • 是否有无立即执行功能;
  • 有无返回值;
  • 能否取消。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// immediate是否是触发事件时执行,为true时则是触发事件时执行,为false是则是停止触发事件才执行
function debounce(fn,wait=2000,immediate=true){
let timer,res;
let debounced = function(...args){
if(timer){
clearTimeout(timer);
}
if(immediate){
let callNow = !timer;
timer =setTimeout(()=>{
timer = null;
},wait)
if(callNow){
res = fn.apply(this,args);
}
}else{
timer =setTimeout(()=>{
fn.apply(this,args);
},wait)
}
retrun res;
}
debounced.cancel = function(){
clearTimeout(timer);
}
return debounced;
}

依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数

content.onmousemove = debounce(count,1000);

效果如下

3.4.2 节流函数(throttle)

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, wait) {
let timer;
return function () {
let context = this;
let args = arguments;
if (!timer) {
timer = setTimeout(() => {
timer = null;
fn.apply(context, args);
}, wait);
}
};
}

依旧使用上述绑定 mousemove 事件的例子,通过上面的防抖函数

content.onmousemove = throttle(count,1000);

效果如下

3.4.3 区别

防抖,连续触发事件时,最多只会执行一次,停止后,再次触发,重新计时;节流,连续触发事件时,n 秒内只会执行一次,但是会继续执行,只是稀释了函数执行的次数。

3.4.4 应用场景

3.5 慎用 with 和 eval

JavaScript 引擎会在编译阶段进行性能优化,其中有些优化依赖于根据代码的词法进行静态分析,并预先确定所有变量的位置和函数定义位置,才能在执行过程中快速的找到标识符。但是如果引擎在代码中发现了 eval() 或者 with,它就只能简单的假设关于标识符的位置判断都是无效的,因为无法在词法分析阶段明确的知道 eval() 会接收什么代码,这些代码会如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底是什么。这个两个的机制会‘欺骗’词法作用。

3.6 减少作用域链查找

js 代码在执行的时候,如果需要访问一个变量或者一个函数的时候,它需要遍历当前执行环境的作用域链,而遍历是从这个作用域链的前端一级一级的向后遍历,直到全局执行环境,所以这里往往会出现一个情况,那就是如果我们需要经常访问全局环境的变量对象的时候,我们每次都必须在当前作用域链上一级一级的遍历,显然这是比较耗时的。

优化方法:
  使用局部变量来缓存全局变量,来减少作用域链上的查找次数

1
2
3
4
5
6
7
8
// 全局变量
var globalVar = 1;
function myCallback(info){
for( var i = 100000; i > 0; i--;){
//每次访问 globalVar 都需要查找到作用域链最顶端,本例中需要访问 100000 次
globalVar += i;
}
}

优化后:

1
2
3
4
5
6
7
8
9
10
11
12
// 全局变量
var globalVar = 1;
function myCallback(info){
//局部变量缓存全局变量
var localVar = globalVar;
for( var i = 100000; i > 0; i--;){
//访问局部变量是最快的
localVar += i;
}
//本例中只需要访问 2次全局变量。在函数中只需要将 globalVar 中内容的值赋给 localVar
globalVar = localVar;
}

3.7 合理使用闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
var a = 2;

function bar() {
console.log(2);
}

return bar;
}

var baz = foo();

baz(); //2

上边示例中,在 foo() 执行后,通常会将 foo 中的整个内部作用域都会被销毁,因为 js 引擎有垃圾回收器用来释放不再使用的内存空间,由于看上去 foo 中的内容不再会被使用,所以很自然的会考虑对其进行回收。然而,闭包的神奇之处正是可以阻止这件事的发生。事实上内部作用域依然存在,因此没有被回收。

闭包有三大特性:

  • 函数嵌套函数;
  • 函数内部可以引用外部的参数和变量;
  • 参数和变量不会被垃圾回收机制回收。

闭包有些什么好处呢?

  • 变量长期存储在内存中;
  • 避免全局变量的污染。

而闭包的缺点,恰恰也就是由这些优点带来的

  • 常驻内存必然增加内存的使用量;
  • 使用不当很容易造成内存泄漏。

以下两篇博客比较详细的讲了闭包、with、eval 对浏览器垃圾回收机制的影响,大家有时间可以看下。
https://zhuanlan.zhihu.com/p/22486908
https://www.cnblogs.com/rubylouvre/p/3345294.html

3.8 数据访问

Javascript 中的数据访问包括直接量 (字符串、正则表达式 )、变量、对象属性以及数组,其中对直接量和局部变量的访问是最快的,对对象属性以及数组的访问需要更大的开销。当出现以下情况时,建议将数据放入局部变量:

  • 对任何对象属性的访问超过 1 次;
  • 对任何数组成员的访问次数超过 1 次;
  • 避免对象嵌套;
  • 还应当尽可能的减少对对象以及数组深度查找。

3.9 使用 GPU.JS 改善 JavaScript 性能

你是否曾经尝试过运行复杂的计算,却发现它需要花费很长时间,并且拖慢了你的进程?

有很多方法可以解决这个问题,例如使用 web worker 或后台线程。GPU 减轻了 CPU 的处理负荷,给了 CPU 更多的空间来处理其他进程。同时,web worker 仍然运行在 CPU 上,但是运行在不同的线程上。

3.9.1 什么是 GPU.js

GPU.js 是一个针对 Web 和 Node.js 构建的 JavaScript 加速库,用于在图形处理单元(GPU)上进行通用编程,它使你可以将复杂且耗时的计算移交给 GPU 而不是 CPU,以实现更快的计算和操作。还有一个备用选项:在系统上没有 GPU 的情况下,这些功能仍将在常规 JavaScript 引擎上运行。

高性能计算是使用 GPU.js 的主要优势之一。如果你想在浏览器中进行并行计算,而不了解 WebGL,那么 GPU.js 是一个适合你的库。

3.9.2 为什么要使用 GPU.js

  • GPU 可用于执行大规模并行 GPU 计算。这是需要异步完成的计算类型;
  • 当系统中没有 GPU 时,它会优雅地退回到 JavaScript;
  • GPU 当前在浏览器和 Node.js 上运行,非常适合通过大量计算来加速网站;
  • GPU.js 是在考虑 JavaScript 的情况下构建的,因此这些功能均使用合法的 JavaScript 语法;

如果你认为你的处理器可以胜任,你不需要 GPU.js,看看下面这个 GPU 和 CPU 运行计算的结果。

如你所见,GPU 比 CPU 快 22.97 倍。

3.9.3 GPU.js 的工作方式

考虑到这种速度水平,JavaScript 生态系统仿佛得到了一个可以乘坐的火箭。GPU 可以帮助网站更快地加载,特别是必须在首页上执行复杂计算的网站。你不再需要担心使用后台线程和加载器,因为 GPU 运行计算的速度是普通 CPU 的 22.97 倍。

gpu.createKernel 方法创建了一个从 JavaScript 函数移植过来的 GPU 加速内核。

与 GPU 并行运行内核函数会有更快的计算速度——快 1-15 倍,这取决于你的硬件。

3.9.4 GPU.js 入门

安装

1
2
3
npm install gpu.js --save
// OR
yarn add gpu.js

在你的项目中要导入 GPU.js。

1
2
3
4
5
6
import { GPU } from ('gpu.js')

// OR
const { GPU } = require('gpu.js')

const gpu = new GPU();

3.9.5 乘法演示

在下面的示例中,计算是在 GPU 上并行完成的。

首先,生成大量数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const getArrayValues = () => {
// 在此处创建2D arrary
const values = [[], []];

// 将值插入第一个数组
for (let y = 0; y < 600; y++) {
values[0].push([]);
values[1].push([]);

// 将值插入第二个数组
for (let x = 0; x < 600; x++) {
values[0][y].push(Math.random());
values[1][y].push(Math.random());
}
}

// 返回填充数组
return values;
};

创建内核(运行在 GPU 上的函数的另一个词)。

1
2
3
4
5
6
7
8
9
10
11
12
const gpu = new GPU();

// 使用 `createKernel()` 方法将数组相乘
const multiplyLargeValues = gpu
.createKernel(function (a, b) {
let sum = 0;
for (let i = 0; i < 600; i++) {
sum += a[this.thread.y][i] * b[i][this.thread.x];
}
return sum;
})
.setOutput([600, 600]);

setOutput:根据输出类型指定输出的预期大小,使用 GPU 加速必须定义输出大小。
this.thread:即为输出大小的宽、高以及深度

使用矩阵作为参数调用内核。

1
2
const largeArray = getArrayValues();
const out = multiplyLargeValues(largeArray[0], largeArray[1]);

输出

1
console.log(out[10][12]); // 记录输出数组第10行和第12列的元素

3.10 其他

  • 使用 requestAnimationFrame 来实现动画;
  • 不要覆盖原生方法。
    无论你的 JavaScript 代码如何优化,都比不上原生方法。因为原生方法是用低级语言写的(C/C++),并且被编译成机器码,成为浏览器的一部分。当原生方法可用时,尽量使用它们,特别是数学运算和 DOM 操作。
  • 查找并删除未使用的代码,删除不必要的部分 CSS,JS 显然会加快网页的加载速度。谷歌的 Chrome 浏览器有这种开箱即用的功能。只需转到查看>开发人员>开发人员工具,并在最近的版本中打开 Sources 选项卡,然后打开命令菜单。然后,选择 Show Coverage,在 Coverage analysis 窗口中高亮显示当前页面上未使用的代码,让您大开眼界。

4 服务器

4.1 启用压缩

4.1.1 Gzip 最佳配置(Nginx)

文件位置:
/nginx/conf/nginx.conf 文件中的 http 标签内

1
2
3
4
5
6
7
8
gzip on;                    #开启gzip压缩功能
gzip_min_length 1k; #设置允许压缩的页面最小字节数; 这里表示如果文件小于1024个字节,就不用压缩,因为没有意义,本来就很小.
gzip_buffers 4 16k; #设置压缩缓冲区大小,此处设置为4个16K内存作为压缩结果流缓存
gzip_http_version 1.1; #压缩版本
gzip_comp_level 4; #设置压缩比率,最小为1,处理速度快,传输速度慢;9为最大压缩比,处理速度慢,传输速度快; 这里表示压缩级别,可以是1到9中的任一个,级别越高,压缩就越小,节省了带宽资源,但同时也消耗CPU资源。
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/javascript application/json; #制定压缩的类型,线上配置时尽可能配置多的压缩类型!
gzip_disable "MSIE [1-6]\."; #配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
gzip_vary on; #选择支持vary header;该选项可以让前端的缓存服务器缓存经过gzip压缩的页面; 这个可以不写,表示在传送数据时,给客户端说明我使用了gzip压缩

4.2 开启 HTTP/2

利用 HTTP2 新特性来提升效率:

多工:HTTP/2 复用 TCP 连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或响应,而且不用按照顺序一一对应,这样就避免了”队头堵塞”。

头信息压缩:HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。
HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。

服务器推送:客户端请求一个网页,这个网页里面包含很多静态资源。正常情况下,客户端必须收到网页后,解析 HTML 源码,发现有静态资源,再发出静态资源请求。其实,服务器可以预测到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了,从而提高的性能。

4.3 SSR 服务端渲染

SPA 模型

SPA 全称是 single page web application。是一种网络应用程序 (WebApp) 模型。在传统的网站中,不同的页面之间的切换都是直接从服务器加载一整个新的页面,而在 SPA 这个模型中,是通过动态地重写页面的部分与用户交互,而避免了过多的数据交换,响应速度自然相对更高。常见的几个 SPA 框架:AgularJS、React、Vue.js。

优点:

  • 页面之间的切换非常快;
  • 一定程度上减少了后端服务器的压力(不用管页面逻辑和渲染);
  • 后端程序只需要提供 API,完全不用管客户端到底是 Web 界面还是手机等。

缺点:

  • 首屏打开速度很慢,因为用户首次加载需要先下载 SPA 框架及应用程序的代码,然后再渲染页面;
  • 不利于 SEO。

SEO

SEO(Search Engine Optimization),中文一般译作:搜索引擎优化。SEO 是一种通过了解搜索引擎的运作规则(如何抓取网站页面,如何索引以及如何根据特定的关键字展现搜索结果排序等)来调整网站,以提高该网站在搜索引擎中某些关键词的搜索结果排名。

SPA 与 SEO 的冲突

既然,SPA 有这么多的优点,但是同时呢又带来了新的问题,SEO 对于提供内容的网站来说是非常重要的。不知谁说的:技术上的问题总有技术去解决,所以呢,SSR 闪亮登场。

SSR

服务端渲染(Server-Side Rendering),是指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程。

优势:

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。

    请注意,截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引。在这里,同步是关键。如果你的应用程序初始展示 loading 图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。也就是说,如果 SEO 对你的站点至关重要,而你的页面又是异步获取内容,则你可能需要服务器端渲染(SSR)解决此问题。

  • 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。无需等待所有的 JavaScript 都完成下载并执行,才显示服务器渲染的标记,所以你的用户将会更快速地看到完整渲染的页面。通常可以产生更好的用户体验,并且对于那些「内容到达时间 (time-to-content) 与转化率直接相关」的应用程序而言,服务器端渲染 (SSR) 至关重要。

使用服务器端渲染 (SSR) 时还需要有一些权衡之处:

  • 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。

  • 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。

  • 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。

SSR 资料

作者

恋上南山

发布于

2021-01-09

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论