0%

优化3.代码优化

JS开销

JS 开销在哪

资源大小相同的情况下,JS 开销更高

Bottom-Up:自下而上,可以看下里面具体做了哪些事情,耗时多久

  • Evaluate Script:解析耗时 101.6ms

The cost of JavaScript in 2019

对于一个网站而言,总共的网络加载过程中,压缩后 1.4 M 的 JS 在整个网络加载耗时中占 1/3

解决方案

  • Code Splitting:代码拆分,按需加载

    当前路径需要哪些资源就加载哪些资源,不需要的延迟加载或访问需要它的页面再加载

  • Tree Shaking:代码减重

    摇一棵树,树上枯萎的叶子就会掉下来。如果有代码用不到,就不打包进来

减少主线程工作量:

  • 避免长任务

  • 避免超过 1KB 的行间脚本

    行间脚本是一个优化策略,比如要加快首屏加载时间,可以把 JS 和 CSS 都行间化,其余通过 Web 文件加载

  • 使用 rAF 和 rAC 进行调度

Progressive Bootstrapping(渐进式启动):

  1. Navigation begins:第一个字节
  2. First Paint:白屏开始有内容绘制
  3. FCP: First Contentful Paint :导航启动
  4. FMP: First Meaningful Paint:页面内容是可见的
  5. Visually ready:页面基本绘制完成
  6. Time to Interactive:页面完全能进行交互
  7. Fully Loaded:结束这轮生命周期

配合 V8 进行优化代码

V8 编译原理

V8中Speculative Optimization简介

当 Chrome 或 Node 要执行一段 JS 代码时,首先会进行解析(Parse it),并将其翻译成一个抽象语法数(AST),之后把文本识别成字符,然后把重要信息提取出来,变成一些节点存储在一定的数据结构里(Interpreter)。最后把代码编成机器码之前,编译器会进行优化工作(Optimize Compiler),但是有时它自动优化工作并不一定合适(逆优化),所以我们需要在代码层面做的优化尽量满足它的优化条件,之后按照它的期望代码去写即可

perf_hooks 性能钩子

注释掉和没注释掉 add(num1, 's') 各执行一次观察 duration 持续时间。在执行函数时,发现参数类型发生变化,运行时不能用已经做过的优化逻辑了,就会把刚做的优化撤销,会造成一定的延时

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
const { performance, PerformanceObserver } = require('perf_hooks')

const add = (a, b) => a + b
const num1 = 1
const num2 = 2

performance.mark('start')
for (let i = 0; i < 10000000; i++) {
add(num1, num2)
}
// add(num1, 's')

for (let i = 0; i < 10000000; i++) {
add(num1, num2)
}
performance.mark('end')

const observer = new PerformanceObserver(list => {
console.log(list.getEntries()[0])
})
observer.observe({ entryTypes: ['measure'] })
performance.measure('测量', 'start', 'end')

/*
// 没注释 add(num1, 's')
PerformanceEntry {
name: '测量',
entryType: 'measure',
startTime: 30.1886,
duration: 50.1463
}
// 注释 add(num1, 's')
PerformanceEntry {
name: '测量',
entryType: 'measure',
startTime: 27.3498,
duration: 19.102599
}
*/

如果想进一步了解 V8 做了什么优化,可以利用 Node 的两个参数(trace-opttrace-deopt

1
node --trace-opt --trace-deopt de-opt.js 

抽象语法数:

  • 源码 -> 抽象语法数 -> 字节码 Bytecode -> 机器码
  • 编译过程进行优化
  • 运行时可能发生反优化

优化机制

  • 脚本流

    脚本正常情况要先进行下载再进行解析最后执行的过程,Chrome 在这里做了优化,在下载过程中可以同时进行解析就可以加快这个过程。当下载一个超过 30 KB 的脚本时,可以先对这 30 KB 内容进行解析,会单独开一个线程去给这段代码进行解析,等整个都下载完在完成时再进行解析合并,最后就可以执行,效率就大大提高了。这是流式处理的一个特点

  • 字节码缓存

    有些东西使用频率比较高,可以把它进行缓存,再次进行访问时就可以加快访问。源码被翻译成字节码之后,发现有一些不仅在当前页面有使用,在其他页面也会有使用的片段,就可以把这些片段对应的字节码缓存起来,在其他页面再次进行访问相同逻辑时,直接从缓存去取即可,不需要再进行翻译过程,效率就大大提高了

  • 懒解析

    对于函数而言,虽然声明了这个函数,但是不一定会马上用它,默认情况下会进行懒解析(先不去解析函数内部的逻辑,当使用时再去解析函数内部逻辑),效率就大大提高了

函数优化

  • lazy parsing 懒解析与 eager parsing 饥饿解析
  • 利用 Optimize.js 优化初次加载时间

懒解析与饥饿解析

  • 只是加一对括号 () 即可把懒解析变为饥饿解析

    但是当我们使用 uglify 进行压缩代码时,这队括号会被去掉,这样就导致本来想做的事情,没办法通知到解析器,这时就可以使用 Optimize.js 工具

    当然现在 uglify 已经把这个问题解决掉了

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
/* test.js */
export default () => {
const add = (a, b) => a*b // lazy parsing
// const add = ((a, b) => a*b) // eager parsing
const num1 = 1
const num2 = 2
add(num1, num2)
}

/* App.jsx */
class App extends React.Component {
constructor(props) {
super(props)
test()
}
}

/* webpack.config.js */
module.exports = {
entry: {
app: './src/index.jsx',
test: './src/test.js',
},
output: {
path: `${__dirname}/build`,
filename: '[name].bundle.js',
},
}

对象优化

做这些优化的目的:迎合 V8 引擎进行解析,把你的代码进行优化。因为它也是用代码写的,所做的优化其实也是代码实现的规则,如果我们的代码迎合了这些规则,就可以帮你去做优化,代码效率可以得到提升

  • 以相同顺序初始化对象成员,避免隐藏类的调整
  • 实例化后避免添加新属性
  • 尽量使用 Array 代替 array-like 对象
  • 避免读取超过数组的长度
  • 避免元素类型转换

以相同顺序初始化对象成员,避免隐藏类的调整

JS 是动态、弱类型语言,写的时候不会声明和强调它变量的类型,但是对于编辑器而言,实际上还是需要知道确定的类型,在解析时,它根据自己的推断,会给这些变量赋一个具体的类型,通常管这些类型叫隐藏类型(hidden class),之后所做的优化都是基于隐藏类型进行的

  • 隐藏类型底层会以描述的数组进行存储,数组里会去强调所有属性声明的顺序,或者说索引,索引的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class RectArea { // HC0
constructor(l, w) {
this.l = l // HC1
this.w = w // HC2
}
}

// 当我们声明了矩形面积类之后,创建第一个HC0
const rect1 = new RectArea(3, 4)
// 接下来再创建实例时,还能按照这个服用所有隐藏类
const rect2 = new RectArea(5, 6)

// car1声明对象的时候会创建一个隐藏类 HC0
const car = { color: 'red' }
// 追加属性会再创建个隐藏类型 HC1
car1.seats = 4

// car2声明时,HC0的属性是关于color的属性,car2声明的是关于seats的属性,所以没办法复用,只能再创建 HC2
const car2 = { seats: 2 }
// HC1不是只包含seats的属性,是包含了color和seats两个属性,所以没有可复用的隐藏类,创建 HC3
car2.color = 'blue'

实例化后避免添加新属性

1
2
3
4
// In-Object属性
const car = { color: 'red' }
// Normal/Fast属性,存储property store里,需要通过描述数组间接查找
car1.seats = 4

尽量使用 Array 代替 array-like 对象

  • array-like 对象:JS 里都有一个 arguments 对象,包含了函数参数变量的信息,本身是一个对象,但是可以通过索引去访问里面的属性,它还有 length 属性,像是一个数组,但又不是数组,不具备数组上面的方法,比如:forEach
  • V8 引擎会对数组能极大性能优化,目前有 21 种不同的元素类型,最好是把类数组转成数组再进行遍历,这样会比不去转成数组直接遍历效率高
1
2
3
4
5
6
7
8
9
// 不如在真实数组上效率高
Array.prototype.forEach.call(arrObj, (val, index) => {
console.log(`${index}${value}`)
})

const arr = Array.prototype.slice.call(arrObj, 0)
arr.forEach((value, index) => {
console.log(`${index}${value}`)
})

避免读取超过数组的长度

  1. arr[3] -> undefined 会与数字进行比较(JS 里越界不一定会报错)
  2. 如果在数组对象里找不到,会沿着原型链向上找,所以会进行额外的开销
  3. 越界比较会造成原型链额外的查找,性能相差 6 倍
1
2
3
4
5
6
7
8
9
10
11
function foo(arr) {
// 把超过边界的值也比较进来 <=
Array.prototype['3'] = 10000
for (let i = 0; i <= arr.length; i++) {
if (arr[i] > 1000) {
console.log(arr[i])
}
}
}

foo([10, 100, 1000])

避免元素类型转换

你可能不知道的V8数组优化

JavaScript 在 V8 中的元素种类及性能优化

  • 对于编辑器而言,实际上是有类型的

    JavaScript 是不区分整数、浮点数和双精度,它们都是数字,但是在编辑器里会对这个做出精确的区分,如果使数组里面类型发生变化,就会造成额外的开销,效率就不高了

1
2
3
4
const arr = [3, 2, 1] // PACKED_SMI_ELEMENTS
arr.push(4.4) // PACKED_DOUBLE_ELEMENTS
arr.push('x') // PACKED_ELEMENTS
arr[9] = 1 // HOLEY_ELEMENTS

类型越具体,编辑器能做的优化就越多,如果越通用,能做的优化余地就越少

  • 只能通过格子向下过度,一旦将单精度浮点数添加到 Smi 数组中,即使稍后用 Smi 覆盖浮点数,它也会被标记为 DOUBLE。类似地,一旦在数组中创建了一个洞,它将永久标记为有洞 HOLEY,即使稍微填充它也是如此

HTML 优化

  • 减少 iframes 使用
  • 压缩空白符
  • 避免节点深层次嵌套
  • 避免使用 table 布局
  • 删除注释
  • CSS 和 JavaScript 尽量使用外联
  • 删除元素默认属性

减少 iframes 使用:

  • 额外添加了文档,需要加载的过程,也会阻碍父文档的加载过程,也就是说如果它加载不完成,父文档本身的 onload 事件就不会触发,一直等着它。在 iframe 里创建的元素,比在父文档创建同样的元素,开销要高出很多

    如果非得用 iframe,可以做个延时加载

    1
    2
    3
    4
    <iframe id="iframe"></iframe>
    <script>
    document.getElementById('iframe').setAttribute('src', url)
    </script>

压缩空白符:

  • 编程时,为了方便阅读,会留空行和换行,最后打包要把空白符去掉

避免节点深层次嵌套:

  • 嵌套越深消耗越高,节点越多最后生成 DOM 树占用内存会比较高

避免使用 table 布局:

  • table 布局本身有很多问题,使用起来没有那么灵活,造成的开销比较大

删除元素默认属性:

  • 本身默认那些值,没必要写出来,写出来就添加了额外的字符,造成了不必要的浪费

借助工具

  • html-minifier

CSS 优化

  • 利用 DevTools 测量样式计算开销

    CSS 解析原则是自右向左去读,会先找出最具体的元素,把所有 a 全部找出来,再根据 #box 进行过滤…直至把所有有影响的元素全部过滤出来。不过随着浏览器解析不断进度,现在这种复杂度的计算依据不是主要的问题了

    1
    .list:nth-last-child(1) > #box a

CSS 优化:

  • 降低 CSS 对渲染的阻塞

  • 利用 GPU 完成动画,不进行回流重绘

  • 使用 font-display 属性

    可以帮助我们让文字更早显示在页面上,减轻文字闪动

  • 使用 contain 属性

    contain 是开发者和浏览器进行沟通的一个属性,通过 contain: layout 告诉浏览器,盒子里所有的子元素和盒子外面的元素之间没有任何布局上的关系。这样浏览器就可以对盒子里面的元素进行单独处理,不需要管理页面上其他的部分,这样就可以大大减少回流计算