webpack打包
在资源请求的过程中,涉及到网络请求的,包括:HTTP、TCP、DNS。其中TCP、DNS前端能做的工作非常有限,因此「优化HTTP」就成为了首要任务。
HTTP的优化可以从三个点出发:
- 减少请求次数;
- 减少每次请求的资源大小;
- 减少请求所花费的时间;
从前端的角度看,其实就是资源的压缩和合并,还有资源的缓存。这些事情webpack十分适合。
webpack性能瓶颈
webpack打包有两个瓶颈:
- webpack 的构建过程「太花时间」
- webpack 打包的结果「体积太大」
webpack优化方案
不要让loader做太多事情。比如说当使用「babel-loader」时,用 include 或 exclude 来帮我们避免不必要的转译。
代码语言:javascript复制module: {
rules: [
{
test: /.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
除此之外,我们可以选择「开启缓存」将转译结果缓存至文件系统。只需要这么写:loader: 'babel-loader?cacheDirectory=true'
优化第三方库
为了防止每次重复打包不会变动的第三方库,可以使用「DllPlugin」。
「DllPlugin」 是基于 Windows 动态链接库(dll)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。「这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包」。
Happypack
「webpack」是单线程的,「Happypack」会充分释放 CPU 在多核并发方面的优势,帮我们把任务分解给多个子进程去并发执行,大大提升打包效率。
下面是用法:
代码语言:javascript复制const HappyPack = require('happypack')
// 手动创建进程池
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
module: {
rules: [
...
{
test: /.js$/,
// 问号后面的查询参数指定了处理这类文件的HappyPack实例的名字
loader: 'happypack/loader?id=happyBabel',
...
},
],
},
plugins: [
...
new HappyPack({
// 这个HappyPack的“名字”就叫做happyBabel,和楼上的查询参数遥相呼应
id: 'happyBabel',
// 指定进程池
threadPool: happyThreadPool,
loaders: ['babel-loader?cacheDirectory']
})
],
}
压缩构建结果体积
首先我们需要知道打包的结果中,哪些资源是比较大的,可以使用webpack-bundle-analyzer[1]来通过可视化的方式查看各个模块的大小以及依赖关系。
使用方式如下:
代码语言:javascript复制const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
其次,我们可以通过「tree-shaking」来删除多余的代码,该功能新版webpack
默认使用。
最后,我们可以通过Gzip
压缩来达到减少资源体积的目的。
按需加载
最经典的优化方式就是路由懒加载,只有当需要加载某个页面的时候,再去动态获取js文件。
vue-router
中路由懒加载的写法如下:
{
path: '/ruleResult',
component: () => import('@/views/rule/index'),
},
图片优化
时下应用较为广泛的 Web 图片格式有「JPEG/JPG、PNG、WebP、Base64、SVG」 等。
计算机中,像素用二进制数表示。一个二进制位表示两种颜色(0|1 对应黑|白),如果一种图片格式对应的二进制位数有 n
个,那么它就可以呈现 2^n
种颜色。
JPEG/JPG
关键字:「有损压缩、体积小、加载快、不支持透明」
「JPG」 适用于呈现色彩丰富的图片,在我们日常开发中,JPG 图片经常作为大的「背景图、轮播图或 Banner 图」出现。
JPG呈现大图,保证质量,体积不大。
JPG的缺点是:当它处理「矢量图形」和 「Logo」 等线条感较强、颜色对比强烈的图像时,人为压缩导致的图片模糊会相当明显。JPEG 图像「不支持透明度处理。」
PNG
关键字:「无损压缩、质量高、体积大、支持透明」
「PNG」(可移植网络图形格式)是一种「无损压缩」的高保真的图片格式。8 和 24,这里都是二进制数的位数。按照我们前置知识里提到的对应关系,8 位的 PNG 最多支持 256 种颜色,而 24 位的可以呈现约 1600 万种颜色。
缺点就是体积太大。主要用它来呈现小的 「Logo」、颜色简单且对比强烈的「图片或背景」等。
SVG
关键字:「文本文件、体积小、不失真、兼容性好」
SVG(可缩放矢量图形)是一种「基于 XML 语法的图像格式」。它和本文提及的其它图片种类有着本质的不同:SVG 对图像的处理不是基于像素点,而是「基于对图像的形状描述」。
SVG的优点:文件体积更小,可压缩性更强。「图片可以无限放大不失真」。SVG是文本文件,比较灵活。SVG的局限性:渲染成本比较高,存在学习成本。
用法:可以采用svg 标签进行编程,也可以通过.svg 文件进行引入。
代码语言:javascript复制<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<circle cx="50" cy="50" r="50" />
</svg>
<img src="文件名.svg" alt="">
Base64
关键字:「文本文件、依赖编码、小图标解决方案」
Base64 并非一种图片格式,而是一种编码方式。作为小图标或者小尺寸图片的解决方案。
base64的优点:「Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,我们可以直接将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数。」
满足以下条件,可以使用base64:
- 图片的「实际尺寸很小」
- 图片无法以雪碧图的形式与其它小图结合(合成雪碧图仍是主要的减少 HTTP 请求的途径,Base64 是「雪碧图的补充」)
- 图片的「更新频率非常低」(不需我们重复编码和修改文件内容,维护成本较低)
WebP
全能型选手,支持「有损压缩和无损压缩」。
WebP的优点:支持「有损压缩和无损压缩」。支持透明、可以显示动图、压缩后尺寸更小。
WebP的缺点:兼容性问题、增加服务器的负担。
浏览器缓存
「浏览器缓存机制有四个方面」,它们按照获取资源时请求的优先级依次排列如下:
- Memory Cache
- Service Worker Cache
- HTTP Cache
- Push Cache
HTTP缓存
HTTP缓存分为「强缓存」和「协商缓存」。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。
强缓存是利用 http 头中的 「Expires」 和 「Cache-Control」 两个字段来控制的。若命中强缓存,则不再请求服务器。命中强缓存的情况下,返回的 HTTP 状态码为 200。
强缓存
强缓存的头部包括「expires」和 「cache-control。」
「expires」 是一个时间戳,如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和 expires 的时间戳,如果本地时间小于 expires 设定的过期时间,那么就直接去缓存中取这个资源。若服务器与客户端存在时差,将带来意料之外的结果。
代码语言:javascript复制expires: Wed, 11 Sep 2019 16:12:18 GMT
Cache-Control
中的max-age
字段也允许我们通过设定「相对的时间长度」来达到同样的目的。「max-age」的单位是秒。相对时间可以规避掉时差问题。客户端会记录下请求到资源的时间点。
cache-control: max-age=31536000
Cache-Control
的 max-age
配置项相对于 expires
的优先级更高。当 Cache-Control
与 expires
同时出现时,我们以 Cache-Control
为准。
cache-control: max-age=3600, s-maxage=31536000
s-maxage
用于表示 cache
服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public
缓存有效。s-maxage
仅在代理服务器中生效,客户端中我们只考虑max-age
。
协商缓存
协商缓存机制下,「浏览器需要向服务器去询问」缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。如果服务器判断资源没有改动,会返回304。
协商缓存的实现包括「Last-Modified和Etag」。
「Last-Modified」 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 「Response Headers」 返回:
代码语言:javascript复制Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT
随后我们每次请求时,会带上一个叫 「If-Modified-Since」 的时间戳字段,它的值正是上一次 response 返回给它的 last-modified 值:
代码语言:javascript复制If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
服务器接受到该请求头时,会和资源最后修改时间进行对比。如果资源有变动,就会返回一个完整的响应内容,并在 「Response Headers」 中添加新的「Last-Modified」 值;否则,返回如上图的 304 响应,「Response Headers」 不会再添加「Last-Modified」 字段。
使用 Last-Modified
存在一些弊端:
- 编辑了文件,但是文件内容没有变化。但是服务器以为有变动。
- 修改文件速度过快,「If-Modified-Since」 只能检查到以秒为最小计量单位的时间差,因此服务器以为没有变动。
此时,需要使用Etag
进行补充。
「Etag」是由服务器为每个资源生成的唯一的「标识字符串」,这个标识字符串是「基于文件内容编码」的,只要文件内容不同,它们对应的 Etag 就是不同的。当首次请求时,我们会在响应头里获取到一个最初的标识符字符串。
代码语言:javascript复制ETag: W/"2a3b-1602480f459"
当再次请求资源时,请求头里会携带一个值相同的、名为「If-None-Match」 的字符串:
代码语言:javascript复制If-None-Match: W/"2a3b-1602480f459"
Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。
Etag
在感知文件变化上比 Last-Modified
更加准确,优先级也更高。当 Etag
和 Last-Modified
同时存在时,以 Etag
为准。
MemoryCache
「MemoryCache」,是指存在内存中的缓存。优先级高,效率也高。
当tab页关闭时,内存里的缓存也消失。哪些文件被放入缓存取决于浏览器,一般较小的文件会放入内存的缓存,大文件则不会。
Service Worker Cache
借助 「Service worker」实现的离线缓存就称为「Service Worker Cache」。
Push Cache
「Push Cache」 是指 HTTP2 在 server push 阶段存在的缓存。
- Push Cache 是缓存的最后一道防线。浏览器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的情况下才会去询问 Push Cache。
- Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。
- 不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。
本地存储
Cookie
「Cookie」可以携带用户信息,当服务器检查 Cookie 的时候,便可以获取到客户端的状态。
Cookie以键值对的形式存在。Cookie最大存储「4KB」的内容。「同一个域名下的所有请求,都会携带 Cookie」。
Web Storage
「Web Storage」分为 「Local Storage」 与「Session Storage」。
两者的区别在于「生命周期」和「作用域」的不同。
- 生命周期:
Local Storage
是持久化的本地存储,存储在其中的数据是永远不会过期的,使其消失的唯一办法是手动删除;而Session Storage
是临时性的本地存储,它是「会话级别的存储」,当会话结束(页面被关闭)时,存储内容也随之被释放。 - 作用域:
Local Storage
、Session Storage
和Cookie
都遵循同源策略。但Session Storage
特别的一点在于,即便是相同域名下的两个页面,只要它们「不在同一个浏览器窗口中」打开,那么它们的Session Storage
内容便无法共享。
「Web Storage」的特点是存储容量大,可以达到「5-10M」之间。不与服务端发生通信。
两者只能用于存储少量的简单数据。当遇到大规模的、结构复杂的数据时,就不适用了。
IndexedDB
「IndexedDB」 是一个「运行在浏览器上的非关系型数据库」。理论上来说,IndexedDB 是没有存储上限的(一般来说不会小于 250M)。它不仅可以存储字符串,还可以存储二进制数据。
浏览器渲染
「解析 HTML」
在这一步浏览器执行了所有的加载解析逻辑,在解析 HTML 的过程中发出了页面渲染所需的各种外部资源请求。
「计算样式」
浏览器将识别并加载所有的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。
「计算图层布局」
页面中所有元素的相对位置信息,大小等信息均在这一步得到计算。
「绘制图层」
在这一步中浏览器会根据我们的 DOM 代码结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码。
「整合图层,得到页面」
最后一步浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,我们有时会手动区分不同的图层)。
css优化
浏览器解析css是从右往左匹配规则。我们要做到:
- 避免使用通配符。
- 关注可以通过继承实现的属性,避免重复匹配重复定义。
- 少用标签选择器。
- 减少嵌套。
CSS和JS加载顺序优化
默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,「不会渲染任何已处理的内容」。因此:需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
解决方案:将CSS下载链接放到head标签内、使用CDN加载静态资源、合理使用「preload」和「prefetch」。
「JS 引擎是独立于渲染引擎存在的」。当 HTML 解析器遇到一个 script
标签时,它会暂停渲染过程,将控制权交给 JS 引擎。也就是说,JS 引擎抢走了渲染引擎的控制权。可以通过对它使用 「defer」和 「async」 来避免不必要的阻塞。
JS有三种加载模式:
- 正常模式。默认情况,JS会阻塞浏览器。
async
模式。JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的。脚本加载结束会「立即执行」。defer
模式。JS 不会阻塞浏览器。它的加载是异步的,「执行被推迟」。等整个文档解析完成、DOMContentLoaded
事件即将被触发时,被标记了defer
的 JS 文件才会开始依次执行。
DOM优化
回流和重绘
「回流」:当对DOM的修改引发了DOM尺寸的变化时,浏览器需要重新计算元素的几何属性,然后将结果进行绘制。该过程为回流。
「重绘」:当对DOM的修改引发了样式的变化,但是没有尺寸变化时,浏览器不需要重新计算元素的几何属性,直接绘制新的样式。该过程为重绘。
结论:「回流一定导致重绘,重绘不一定导致回流。」
导致回流的操作有:
- 改变 DOM 元素的几何属性:修改诸如「width、height、padding、margin、left、top、border」等。
- 改变 DOM 树的结构:节点的「增删和移动」。
- 获取特定属性的值:诸如「offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight」以及「getComputedStyle」 方法。因为浏览器需要实时计算最新的值,会进行回流。
优化方案有:
- 缓存特定属性的值,防止频繁获取导致频繁回流。
- 避免逐条改变样式,使用类名去合并样式。
- 将DOM离线。也就是
display: none
。
减少DOM操作
使用document.fragment
来减少DOM操作。
let container = document.getElementById('container')
// 创建一个DOM Fragment对象作为容器
let content = document.createDocumentFragment()
for(let count = 0; count < 10000; count ){
// span此时可以通过DOM API去创建
let oSpan = document.createElement("span")
oSpan.innerHTML = '我是一个小测试'
// 像操作真实DOM一样操作DOM Fragment对象
content.appendChild(oSpan)
}
// 内容处理好了,最后再触发真实DOM的更改
container.appendChild(content)
Flush队列
当频繁回流或者重绘的时候,浏览器缓存一个flush队列,把我们触发的回流与重绘任务都塞进去,待到队列里的任务多起来、或者达到了一定的时间间隔,或者“不得已”的时候,再将这些任务一口气出队。
而获取特定属性的值就是不得已的时候,因此要避免频繁获取。
懒加载
懒加载:它是针对图片加载时机的优化。当页面上图片较多时,如果不做额外的处理,浏览器会将所有资源进行加载。如此会造成白屏、卡顿的不良影响。
懒加载的核心思路是:当元素出现在可视区域内,style 内联样式中的背景图片属性从 none 变成了一个在线图片的 URL。
在懒加载的实现中,有两个关键的数值:一个是「当前可视区域的高度」,另一个是「元素距离可视区域顶部的高度」。
「当前可视区域的高度」, 在和现代浏览器及 IE9 以上的浏览器中,可以用 window.innerHeight
属性获取。
而「元素距离可视区域顶部的高度」,选用 getBoundingClientRect()
方法来获取返回元素的大小及其相对于视口的位置。
// 获取所有图片标签
const imgs = document.getElementsByTagName('img');
const viewHeight = window.innerHeight;
// 元素距离顶部的距离比可是区域高度小,就意味着元素可以被用户看见,需要加载
const isView = (e) => (viewHeight - e.getBoundingClientRect().top) >= 0
// 记录检查到哪一张图片
let num = 0;
const lazyload = () => {
for (let i = num; i < imgs.length; i ) {
let currentEl = imgs[i];
if (isView(currentEl)) {
currentEl.src = currentEl.dataset.src;
num = i 1;
}
}
}
// 监听Scroll事件
window.addEventListener('scroll', lazyload, false);
需要注意的是,上面监听的滚动事件会频繁触发,因此需要进行防抖处理。
防抖与节流
原生事件中,有许多事件容易频繁触发。比如scroll 事件、resize事件、鼠标事件、键盘事件等等。频繁触发回调导致的大量计算会引发页面的抖动甚至卡顿。因此需要防抖和节流来「限制触发的频率」。
节流
所谓的“节流”,是通过在一段时间内「无视后来产生的回调请求」来实现的。也就是说,一段时间内,以「第一个请求」为准。这段时间所有的其他请求都被忽略。
代码语言:javascript复制function throttle(fn, interval) {
// 通过闭包保存上一次触发回调的时间
let last = 0;
return function() {
// 保留调用时的this上下文
let context = this;
// 保留调用时传入的参数
let args = arguments;
// 记录本次触发回调的时间
let now = new Date();
// 时间间隔大于指定时间阈值,则触发回调
if (now - last >= interval) {
last = now;
fn.apply(context, args);
}
}
}
const better_scroll = throttle(() => console.log('done'), 1000);
document.addEventListener('scroll', better_scroll);
防抖
所谓“防抖”,就是在某段时间内,不管你触发了多少次回调,都只认最后一次。也就是说,一段时间内,以「最后一个请求」为准。
代码语言:javascript复制function debounce(fn, delay) {
let timer = null
return function() {
let context = this;
let args = arguments;
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
const better_scroll = debounce(() => console.log('done'), 1000)
document.addEventListener('scroll', better_scroll);
节流和防抖合体
代码语言:javascript复制// fn是我们需要包装的事件回调, delay是时间间隔的阈值
function throttle(fn, delay) {
// last为上一次触发回调的时间, timer是定时器
let last = 0, timer = null
// 将throttle处理结果当作函数返回
return function () {
// 保留调用时的this上下文
let context = this
// 保留调用时传入的参数
let args = arguments
// 记录本次触发回调的时间
let now = new Date()
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
if (now - last < delay) {
// 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
clearTimeout(timer)
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, delay)
} else {
// 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应
last = now
fn.apply(context, args)
}
}
}
// 用新的throttle包装scroll的回调
const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
总结
节流是技能需要冷却,再次使用需要等待;防抖是打断技能释放,以最后一次释放为准。
总结
上面是阅读《前端性能优化原理与实践》小册后所总结的所有性能优化的知识点,希望对看到本文的你有所帮助。
Reference
[1]
webpack-bundle-analyzer: https://www.npmjs.com/package/webpack-bundle-analyzer