不折腾的前端,和咸鱼有什么区别
| 目录 |
|---|
| 一 目录 |
| 二 前言 |
| 2.1 DNS 解析 |
| 2.2 TCP 连接 |
| 2.3 发送 HTTP 请求 |
| 2.4 服务器响应 |
| 2.5 浏览器解析渲染页面 |
| 2.6 其他 |
| 2.7 小结 |
| 三 浏览器缓存 |
| 3.1 缓存位置 |
| 3.2 缓存机制 |
| 四 Cookie、Web Storage 和 IndexDB |
| 4.1 Cookie |
| 4.2 Local Storage |
| 4.3 Session Storage |
| 4.4 IndexDB |
| 五 CDN |
| 六 负载均衡 |
| 七 Webpack 优化 |
| 7.1 针对 Webpack 本身构建优化 |
| 7.1.1 优化 resolve.modules 配置 |
| 7.1.2 优化 resolve.extensions 配置 |
| 7.2 通过 Loader 和 Plugin 优化 |
| 7.2.1 babel-loader |
| 7.2.2 tree shaking |
| 7.2.3 可视化分析 |
| 7.2.4 缓存 |
| 7.2.5 多进程 |
| 7.2.6 抽离 |
| 7.2.7 多进程代码压缩 |
| 7.2.8 拆包 |
| 7.2.9 打包资源压缩 |
| 7.2.10 按需加载 |
| 7.3 优化体验 |
| 八 图片优化 |
| 8.1 JPEG 与 JPG |
| 8.2 PNG-8 与 PNG-24 |
| 8.3 GIF |
| 8.4 SVG |
| 8.5 Base64 |
| 8.6 雪碧图 |
| 8.7 WebP |
| 九 Gzip 压缩 |
| 十 服务端渲染 |
| 10.1 客户端渲染和服务端渲染 |
| 10.2 解决的性能问题 |
| 10.3 如何使用服务端渲染 |
| 10.4 服务端渲染小结 |
| 十一 浏览器渲染机制 |
| 11.1 浏览器渲染步骤 |
| 11.2 优化 - CSS 选择器问题 |
| 11.3 优化 - CSS 加载问题 |
| 11.4 优化 - JS 加载问题 |
| 11.5 优化 - DOM 渲染问题 |
| 十二 预加载页面资源 |
| 十三 长列表 |
| 13.1 懒加载 |
| 13.2 可视区域渲染 |
| 十四 性能监控 |
| 十五 参考文献 |
| 15.1 Webpack 优化 |
| 15.2 其他优化 |
要说起前端性能优化,其实我们可以从 “输入 URL 到页面呈现” 这个知识点着手讲起。
在用户输入 URL,按下回车之后,走过的步骤:
这其中可以做到哪些优化呢?
jsliang 在这里将这些知识点一锅炖,看你吃下多少。
DNS 解析过程是一个知识点,详细可看:计算机网络 - DNS
首先需要知道的是 DNS 解析的开始步骤:浏览器 DNS 缓存 -> 系统缓存(host) -> 路由器缓存
浏览器 DNS 缓存:你不确定,也无法帮用户缓存;
系统缓存(host):你自己修改 host 文件都要权限,修改用户的就更不靠谱了;
路由器缓存:用户家的路由器……
然后本地服务器向根服务器、顶级域名服务器、主域名服务器这些的请求就更不用说了,前端没法接触。
所以这个步骤我们忽略先。
这个步骤我们也忽略,前端性能优化暂时管不到它。
发送 HTTP 请求这块,我们可以通过 4 点进行讲解:
HTTP 请求发起的时候,我们可以利用浏览器缓存,看采用强缓存还是协商缓存,这样我们对于有缓存的页面可以快速加载。
利用 Cookie 和 WebStorage 对一些无关紧要的数据进行缓存,方便利用。
静态资源的请求可以采用 CDN,减少服务器压力、防止不必要携带 Cookie 的场景等。
利用负载均衡的特点,开启 Node.js 方面的 PM2 或者 Nginx 的反向代理,轮询服务器,平均各个服务器的压力。
在服务器响应的时候,我们也可以做 4 部分:
在发布项目到服务器之前,我们可以利用一些可视化插件进行分析,使用 Happypack 等提高打包效率,项目内容上可以做按需加载、tree shaking 等。
我们需要熟悉了解 JPG/JPEG、PNG-8/PNG-24、GIF、Base64、SVG 这些图片的特性,然后通过 Webpack 的 url-loader 将一些小图标转换成 Base64,一些 Icon 使用 SVG,一些轮播图、Banner 图用 JPG/JPGE、雪碧图的使用等。
Gzip 压缩的原理是在一个文本文件中找一些重复出现的字符串、临时替换它们,从而使整个文件变小(对于图片等会处理不了)。我们可以通过 Webpack 的 ComparessionPlugin 进行 Gzip 压缩,然后在 Nginx 上进行配置,就可以利用好 Gzip 了。
服务端渲染是指浏览器请求的时候,服务端将已经渲染好的 HTML 页面直接返回给浏览器,浏览器直接加载它的 HTML 渲染即可,减少了前后端交互,对 SEO 更友好。
浏览器解析渲染页面的过程是:

DOM 树CSS 规则树(CSS Rule Tree)DOM Tree 和 CSS Rule Tree 相结合,生成 渲染树(Render Tree)Layout of the render tree)。Painting the render tree)关于这个步骤我们的优化方案有:
head 位置加载 CSS,减少 HTML 加载完毕需要等待 CSS 加载的问题。script 标签通常放 body 后面,同时可以利用 script 标签的 async 和 defer 属性,同步加载 JS 或者等 HTML 和 CSS 加载渲染完后再加载 JS。如何避免触发回流:
visibility 替换 displaytable 布局。对于 Render Tree 的计算通常只需要遍历一次就可以完成,但是 table 布局需要计算多次,通常要花 3 倍于等同元素的时间,因此要避免。width、height 等会触发回流的操作。除此之外,我们还可以通过:
preload 预加载页面等进行性能优化相关操作。
以上,我们就通过 6 个部分,串起来讲解了前端性能优化部分的知识点。
当然,肯定有我们没有顾及的地方,欢迎小伙伴评论留言吐槽或者私聊 jsliang,jsliang 会逐步完善这块内容。
下面我们逐一详细的过一下上面讲到的优化知识点。
浏览器缓存可以简单地理解为 HTTP 缓存。
浏览器缓存位置分 4 个部分:
Service Worker Cache - 运行在浏览器背后的独立线程。一般可以用来实现缓存功能。Menory Cache - 内存中的缓存。主要是页面上已经下载的样式、脚本、图片等已经抓取到的资源。Disk Cache - 硬盘中的缓存。读取速度相对慢点。Push Cache - 推送缓存。 是 HTTP2 中的内容,当以上 3 种缓存都没有命中的时候,它才会被使用。强缓存优先于协商缓存进行,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存。强缓存不会向服务器发送请求,直接从缓存中读取资源。
强缓存利用 HTTP 请求头的 Expires 和 Cache-Control 两个字段来控制。
协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器中;生效则返回 304,继续使用缓存。
协商缓存利用 Last-Modified + If-Modified-Since 和 Etag + If-None-Match 来实现。
具体的缓存过程小伙伴们可以看浏览器缓存篇章,这里就不哆嗦了:
Cookie 最开始被设计出来其实并不是来做本地存储的,而是为了弥补 HTTP 在状态管理上的不足。
Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储。
向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。
缺陷:
4kb,只能存储少量信息。Cookie 请求每次都会携带上完整的 Cookie,随着请求数增多,造成性能浪费。Local Storge 也是针对同一个域名。
同一个域名下,会存储相同的一段 Local Storage。
相比 Cookie 优势:
5M,大于 Cookie 的 4kb。Cookie 的性能缺陷和安全缺陷。setItem 和 getItem 两个 API 接口。应用场景:
Base64 方式存储官方 Logo 等图片。基本上和 Local Stoarge 一致。
相比较上的不同:
Local Storage 的持续化存储,Session Storage 当页面关闭的时候就不复存在了。应用场景:
IndexedDB 是运行在浏览器中的 非关系型数据库。
因为本质上是数据库,所以一般来说容量是没有上线的。
CDN(Content Delivery Network,内容分发网络)指的是一组分布在各个地区的服务器。
这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。
CDN 提供快速服务,较少受高流量影响。
假设有一部影片出版,非常多人看。jsliang 在广州,请求上海的服务器,结果这个服务器非常多人,资源响应地很慢。于是 jsliang 切换了路线,看到深圳服务器也有这个资源,于是向深圳服务器请求,结果能很快地看到这部影片。
在这个场景中,深圳服务器就扮演 CDN 的角色。
CDN 的核心:缓存 和 回源。
copy 一份到 CDN 服务器。应用场景:
如果是大型网站,负载均衡是不可或缺的内容。
PM2:一款 Node.js 进程管理器,让计算机每一个内核都启动一个 Node.js 服务,并且实现自动控制负载均衡。Nginx:通过轮询机制,将用户的请求分配到压力较小的服务器上(反向代理)。区别:反向代理是对服务器实现负载均衡,而 PM2 是对进程实现负载均衡。
Webpack 的优化瓶颈,主要是 2 个方面:
resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块,默认是 ['node_modules'],但是,它会先去当前目录的 ./node_modules 查找,没有的话再去 ../node_modules,最后到根目录。
所以可以直接指定项目根目录,就不需要一层一层查找。
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
}在导入没带文件后缀的路径时,Webpack 会自动带上后缀去尝试询问文件是否存在,而 resolve.extensions 用于配置尝试后缀列表;默认为 extensions:['js', 'json']。
当遇到 require('./data') 时 Webpack 会先尝试寻找 data.js,没有再去找 data.json;如果列表越长,或者正确的后缀越往后,尝试的次数就会越多。
所以在配置时为提升构建优化需遵守:
js、jsx、json。以 babel-loader 为例,可以通过 include 和 exclude 帮助我们避免 node_modules 这类庞大文件夹。
通过 ES6 的 import/export 来检查未引用代码,以及 sideEffects 来标记无副作用代码,最后用 UglifyJSPlugin 来做 tree shaking,从而删除冗余代码。
speed-measure-webpack-plugin:测量出在构建过程中,每一个 Loader 和 Plugin 的执行时长。webpack-bundle-analyzer:通过矩阵树图的方式将包内各个模块的大小和依赖关系呈现出来。webpack-chartwebpack-analysecache-loader参考链接:cache-loader
在 babel-loader 开启 cache 后,将 loader 的编译结果写进硬盘缓存,再次构建如果文件没有发生变化则会直接拉取缓存。
uglifyjs-webpack-plugin也可以解决缓存问题。
Happypack 可以将任务分解成多个子进程去并发执行,大大提升打包效率。
通过 DllPlugin 或者 Externals 进行静态依赖包的分离。
由于 CommonsChunkPlugin 每次构建会重新构建一次 vendor,所以出于效率考虑,使用 DllPlugin 将第三方库单独打包到一个文件中,只有依赖自身发生版本变化时才会重新打包。
因为自带的 UglifyJsPlugin 压缩插件是单线程运行的,而 ParallelUglifyPlugin 可以并行执行。
所以通过 ParallelUglifyPlugin 代替自带的 UglifyJsPlugin 插件。
在 Webpack 中,到底什么是代码分离?代码分离允许你把代码拆分到多个文件中。如果使用得当,你的应用性能会提高很多。因为浏览器能缓存你的代码。
每当你做出一次修改,包含修改的文件需要被所有访问你网站的人重新下载。但你并不会经常修改应用的依赖库。
如果你能把那些依赖库拆分到完全分离的文件中,即使业务逻辑发生了更改,访问者也不需要再次下载依赖库,直接使用之前的缓存就可以了。
由于有了 SplitChunksPlugin,你可以把应用中的特定部分移至不同文件。如果一个模块在不止一个 chunk 中被使用,那么利用代码分离,该模块就可以在它们之间很好地被共享。
UglifyJSPluginHtmlWebpackPluginsplitChunks.cacheGroupsMiniCssExtractPlugin通过 Code-Splitting 来做 React 的按需加载.
Code_Splitting 核心是 require-ensure。
.svg 后缀的文件进行引用。img src 会发起资源请求,但是 Base64 得到的是字符串,嵌入 HTML 中)url-loader 可以根据文件大小来判断是否编码成 Base64。雪碧图、CSS 精灵、CSS Sprites、图像精灵,都是同一个玩意。
它是将小图标和背景图像合并到一张图片上,然后通过 CSS 背景定位来显示其中的每一个具体部分。
它是一种优化手段,因为单张图片所需的 HTTP 请求更少,对内存和带宽更加友好。
通过 compression-webpack-plugin 可以开启 Gzip 压缩。
如果压缩文件太小,那不使用;但是如果具有一定规模的项目文件,可以开启 Gzip。
Gzip 并不是万能的,它的原理是在一个文本文件中找一些重复出现的字符串、临时替换它们,从而使整个文件变小,所以对于图片等会处理不了。
服务器压缩也需要时间开销和 CPU 开销,所以有时候可以用 Webpack 来进行 Gzip 压缩,从而为服务器分压。
客户端渲染中,页面上呈现的内容,在 HTML 源文件中往往找不到。
而服务端渲染,当用户第一次请求页面时,服务器会把需要的组件或者页面渲染成 HTML 字符串,返回给客户端。
即客户端直接拿到 HTML 内容,而不需要跑一遍 JS 去生成 DOM 内容。
“所见即所得”,服务端渲染情景下,页面上呈现的内容,在 HTML 源文件里面也可以找到。
假设 A 网站关键字上有 前端性能优化,但是这篇文章只有 A 网站服务器搜索过后才会出来结果,这时候搜索引擎是无法找到的。
为了更好的 SEO 效果,就要拿 “现成的内容” 给搜索引擎看,就要开启服务端渲染。
其次,服务端渲染解决了一个性能问题 —— 首屏加载速度过慢。
从输入 URL 到页面渲染过程中我们知道,如果是客户端渲染,我们需要加载 HTML、CSS,然后再经过 JS 形成 Render Tree,定位后再绘制页面。
这个过程中用户一直在等待,如果采用了服务端渲染,那么服务端可以直接给一个可以拿来呈现给用户的页面。
给 React 开启:
前端项目 - VDOM.js
import React from 'react';
const VDom = () => {
return <div>我是一个被渲染为真实 DOM 的虚拟 DOM</div>
};
export default VDom;Node 项目 - index.js
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import VDom from './VDom';
// 创建一个 express 应用
const app = express();
// renderToString 是把虚拟 DOM 转化为真实 DOM 的关键方法
const RDom = renderToString(<VDom />);
// 编写 HTML 模板,插入转化后的真实 DOM 内容
const Page = `
<html>
<head>
<title>test</title>
</head>
<body>
<span>服务端渲染出了真实 DOM: </span>
${RDom}
</body>
</html>
`;
// 配置 HTML 内容对应的路由
app.get('/index', function(req, res) {
res.send(Page)
});
// 配置端口号
const server = app.listen(8000);VDom 组件已经被 renderToString 转化为了一个内容为 <div data-reactroot="">我是一个被渲染为真实 DOM 的虚拟 DOM</div> 的字符串,这个字符串被插入 HTML 代码,成为了真实 DOM 树的一部分。
至于 Vue 的可以看:服务器端渲染 (SSR)?
不熟悉 Vue,就不哆嗦了。
SSR 主要用于解决单页应用首屏渲染慢以及 SEO 问题,同时也解决了与后端同学的沟通成本。但同时:提高了服务器压力,吃 CPU,内存等资源,优化不好提高成本。
浏览器内核决定了浏览器解释网页语法的方式。
目前常见的浏览器内核有:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。

如上图,浏览器的渲染过程为:
DOM 树CSS 规则树(CSS Rule Tree)DOM Tree 和 CSS Rule Tree 相结合,生成 渲染树(Render Tree)Layout of the render tree)。Painting the render tree)我们正常的阅读顺序是从左往右的,但是 CSS 解析器解析 CSS 的时候,采用的是古人的规则。
#ul li {}这样的一行规则,我们写起来的时候很顺畅:先找 id 为 ul 的元素,再找里面的 li 元素。
但是实际上 CSS 解析器是从右往左的,它会先查找所有 li 元素,并且逐个确认这个 li 元素的父元素的 id 是不是 ul,这就坑死了。
所以像通配符 * { padding: 0; margin: 0 } 这种,小伙伴们就应该减少设置,要不然页面的元素越多遍历匹配越久。
总结一下:
* 等。span 替换为 .span。#ul li a。为了避免 HTML 解析完毕,但是 CSS 没有解析完毕,从而导致页面直接 “裸奔” 在用户面前的问题,浏览器在处理 CSS 规则树的时候,不会渲染任何已处理的内容。
所以很多时候,我们会让网页尽早处理 CSS,即在 head 标签中启用 link 或者启用 CDN 实现静态资源加载速度的优化。
在上面的加载过程中我们并没有提到 JS,实际上 JS 会对 DOM 和 CSSDOM 进行修改,因此 JS 的执行会阻止 CSS 规则树的解析,有时候还会阻塞 DOM。
实际上,当 HTML 解析器遇上 script 标签时,它会暂停解析过程,将控制器交给 JS 引擎。
如果是内部的 JS 代码,它会直接执行,但是如果是 src 引入的,还要先获取脚本,再进行执行。
等 JS 引擎执行完毕后,再交接给渲染引擎,继续 HTML 树和 CSS 规则树的构建。
这样一来一回交接,而且有时候 JS 执行过多还会卡慢,进而导致页面渲染变慢。
所以我们可以通过 async 异步加载完 JS 脚本,再执行里面内容;或者通过 defer 等整个文档解析完毕后,再执行这个 JS 文件。
如果 JS 和 DOM 元素或者其他 JS 代码之间的依赖不强的时候,使用 async。
如果 JS 依赖于 DOM 元素和其他 JS 的执行结果,那就使用 defer。
当使用 JS 去操作 DOM 的时候,实际上是 JS 引擎和渲染引擎之间的沟通,这个沟通的过程要开销的。
每操作一次 DOM 就收费一次,多了页面就卡起来咯。
同时,操作 DOM 的时候修改了尺寸等元素,还会引起回流和重绘。
layout)。当元素的尺寸、结构或者触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。什么操作触发回流?
border、margin、padding、width、height)resize)什么操作触发重绘?
background、color)visibility)background-image)
我们仔细看这张图,可以看到重排(Layout)会导致 Render Tree 重构,进而触发重绘(Painting):
因此,我们操作 DOM 的时候,可以这么优化:
visibility 替换 displaytable 布局。对于 Render Tree 的计算通常只需要遍历一次就可以完成,但是 table 布局需要计算多次,通常要花 3 倍于等同元素的时间,因此要避免。width、height 等会触发回流的操作。preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。
提供的好处主要是:
document 的 onload 事件font 字体隔了一段时间才刷出<!-- 使用 link 标签静态标记需要预加载的资源 -->
<link rel="preload" href="/path/to/style.css" as="style">
<!-- 或使用脚本动态创建一个 link 标签后插入到 head 头部 -->
<script>
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/path/to/style.css';
document.head.appendChild(link);
</script>在不支持 preload 的浏览器环境中,会忽略对应的 link 标签。
区分 preload 和 prefetch:
preload:告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。prefetch:告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。当然,开发中需要注意:
preloadpreload 和 prefetchpreload 加载跨域资源懒加载实现思路:
div 通过背景图片设置为 none,起到占位的作用。div 填写有效 URL。index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Lazy-Load</title>
<style>
.img {
width: 100px;
height: 300px;
}
.img img {
width: 200px;
height: 400px;
}
</style>
</head>
<body>
<div class="container">
<!-- 注意我们并没有为它引入真实的 src -->
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting1.png"></div>
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting2.png"></div>
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting3.png"></div>
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting4.png"></div>
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/AboutMe-painting5.png"></div>
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn6.png"></div>
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn7.png"></div>
<div class="img"><img class="pic" alt="加载中" data-src="https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/bpmn8.png"></div>
</div>
<script>
(function() {
// 获取所有的图片标签
const imgs = document.getElementsByTagName('img');
// 获取可视区域的高度(document.documentElement.clientHeight 是兼容低版本 IE)
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0;
const lazyload = () => {
for (let i = num; i < imgs.length; i++) {
// 用可视区域高度减去元素顶部距离可视区域顶部的高度
let distance = viewHeight - imgs[i].getBoundingClientRect().top;
// 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
if (distance >= 0){
// 给元素写入真实的 src,展示图片
imgs[i].src = imgs[i].getAttribute('data-src');
// 前 i 张图片已经加载完毕,下次从第 i + 1 张开始检查是否露出
num = i + 1;
}
}
}
// 首屏初始化
lazyload();
// 监听Scroll事件
window.addEventListener('scroll', lazyload, false);
})()
</script>
</body>
</html>还有其他方法,诸如:
img 标签自带的 loading 属性InsectionObserver无限滚动在移动端很常见,但是可见区域渲染并不常见,主要是因为 IOS 上 UIWebView 的 onscroll 并不能实时触发。
实现可见区域渲染的思路:
startIndexendIndexstartIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
代码实现:略。
PerformancePage SpeedLighthouse
npm i lighthouse -g、lighthouse https://www.baidu.comLightHouse 的 Audits 面板本篇参考文献有 31 篇。
2019 年文章:
2018 年文章:
2017 年文章:
jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
基于 https://github.com/LiangJunrong/document-library 上的作品创作。
本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。