计算图片大小
- 对于一张 100 * 100 像素的图片来说,图像上有 10000 个像素点,如果每个像素的值是RGBA存储的话,那么也就是说每个像素有 4 个通道,每个通道 1 个字节(8 位 = 1个字节),所以该图片大小大概为 39KB(10000 * 1 * 4 / 1024)。
如何优化图片
- 减少像素点
- 减少每个像素点能够显示的颜色
图片加载优化
不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片。
小图使用 base64 格式
将多个图标文件整合到一张图片中(雪碧图)
选择正确的图片格式:
对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
照片使用 JPEG
DNS 预解析
- DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。
<link rel="dns-prefetch" href="//yuchengkai.cn">
节流
- 滚动事件中会发起网络请求,但是我们并不希望用户在滚动过程中一直发起请求,而是隔一段时间发起一次,对于这种情况我们就可以使用节流。
// func是用户传入需要防抖的函数
// wait是等待时间
const throttle = (func, wait = 50) => {
// 上一次执行该函数的时间
let lastTime = 0
return function(...args) {
// 当前时间
let now = +new Date()
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
}
setInterval(
throttle(() => {
console.log(1)
}, 500),
1
)
- lodash也提供了API https://lodash.com/docs/4.17.15#throttle
_.throttle(func, [wait=0], [options={}])
防抖
- (防止重复点击)有一个按钮点击会触发网络请求,但是我们并不希望每次点击都发起网络请求,而是当用户点击按钮一段时间后没有再次点击的情况才去发起网络请求,对于这种情况我们就可以使用防抖。
// func是用户传入需要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 如果已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
- lodash也提供了API https://lodash.com/docs/4.17.15
_.debounce(func, [wait=0], [options={}])
预加载
- 有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载
预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载
<link rel="preload" href="http://example.com">
- 预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。
预渲染
- 可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染
<link rel="prerender" href="http://example.com">
- 预渲染虽然可以提高页面的加载速度,但是要确保该页面大概率会被用户在之后打开,否则就是白白浪费资源去渲染。
懒执行
- 懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒。
懒加载
懒加载就是将不关键的资源延后加载。
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src
属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src
属性,这样图片就会去下载资源,实现了图片懒加载。
懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等等。
CDN
CDN 的原理是尽可能的在各个地方分布机房缓存数据,这样即使我们的根服务器远在国外,在国内的用户也可以通过国内的机房迅速加载资源。
因此,我们可以将静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。并且对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie,平白消耗流量。
测试性能工具
- Audits (Chrome 已经提供了一个大而全的性能测试工具)
点击 Audits 后,可以看到如下的界面
在这个界面中,我们可以选择想测试的功能然后点击Run audits,工具就会自动运行帮助我们测试问题并且给出一个完整的报告
报告中分别为性能、体验、SEO都给出了打分,并且每一个指标都有详细的评估
工具还提供了一些建议便于我们提高这个指标的分数
- Performance工具
每个时间段中浏览器在处理什么事情,哪个过程最消耗时间,便于我们更加详细的了解性能瓶颈。
JS 性能优化
- JS 是编译型还是解释型语言其实并不固定
- 首先 JS 需要有引擎才能运行起来,无论是浏览器还是在 Node 中,这是解释型语言的特性。
- 在 V8 引擎下,又引入了
TurboFan
编译器,他会在特定的情况下进行优化,将代码编译成执行效率更高的Machine Code,当然这个编译器并不是 JS 必须需要的,只是为了提高代码执行性能,所以总的来说 JS 更偏向于解释型语言。 - JS 代码首先会解析为抽象语法树(AST),然后会通过解释器或者编译器转化为Bytecode或者Machine Code
对于函数来说,应该尽可能避免声明嵌套函数(类也是函数),因为这样会造成函数的重复解析。
- 如果一个函数被多次调用并且参数一直传入
number
类型,那么 V8 就会认为该段代码可以编译为 Machine Code,因为你固定了类型,不需要再执行很多判断逻辑了。 - 如果一旦我们传入的参数类型改变,那么 Machine Code 就会被DeOptimized为 Bytecode,这样就有性能上的一个损耗了。所以如果我们希望代码能多的编译为 Machine Code 并且 DeOptimized 的次数减少,就应该尽可能保证传入的类型一致
const { performance, PerformanceObserver } = require('perf_hooks')
function test(x) {
return x + x
}
// node 10 中才有 PerformanceObserver
// 在这之前的 node 版本可以直接使用 performance 中的 API
const obs = new PerformanceObserver((list, observer) => {
console.log(list.getEntries())
observer.disconnect()
})
obs.observe({ entryTypes: ['measure'], buffered: true })
performance.mark('start')
let number = 10000000
// 不优化代码
%NeverOptimizeFunction(test)
while (number--) {
test(1)
}
performance.mark('end')
performance.measure('test', 'start', 'end')
编译器还有个骚操作Lazy-Compile
- 当函数没有被执行的时候,会对函数进行一次预解析,直到代码被执行以后才会被解析编译。
- 那么有什么办法能够让代码不被预解析呢?
(function test(obj) {
return x + x
})