JS开销
JS 开销在哪
资源大小相同的情况下,JS 开销更高
Bottom-Up
:自下而上,可以看下里面具体做了哪些事情,耗时多久
Evaluate Script
:解析耗时 101.6ms
对于一个网站而言,总共的网络加载过程中,压缩后 1.4 M 的 JS 在整个网络加载耗时中占 1/3
解决方案
Code Splitting
:代码拆分,按需加载当前路径需要哪些资源就加载哪些资源,不需要的延迟加载或访问需要它的页面再加载
Tree Shaking
:代码减重摇一棵树,树上枯萎的叶子就会掉下来。如果有代码用不到,就不打包进来
减少主线程工作量:
避免长任务
避免超过 1KB 的行间脚本
行间脚本是一个优化策略,比如要加快首屏加载时间,可以把 JS 和 CSS 都行间化,其余通过 Web 文件加载
使用 rAF 和 rAC 进行调度
Progressive Bootstrapping(渐进式启动):
- Navigation begins:第一个字节
- First Paint:白屏开始有内容绘制
- FCP: First Contentful Paint :导航启动
- FMP: First Meaningful Paint:页面内容是可见的
- Visually ready:页面基本绘制完成
- Time to Interactive:页面完全能进行交互
- Fully Loaded:结束这轮生命周期
配合 V8 进行优化代码
V8 编译原理
当 Chrome 或 Node 要执行一段 JS 代码时,首先会进行解析(Parse it),并将其翻译成一个抽象语法数(AST),之后把文本识别成字符,然后把重要信息提取出来,变成一些节点存储在一定的数据结构里(Interpreter)。最后把代码编成机器码之前,编译器会进行优化工作(Optimize Compiler),但是有时它自动优化工作并不一定合适(逆优化),所以我们需要在代码层面做的优化尽量满足它的优化条件,之后按照它的期望代码去写即可
注释掉和没注释掉 add(num1, 's')
各执行一次观察 duration
持续时间。在执行函数时,发现参数类型发生变化,运行时不能用已经做过的优化逻辑了,就会把刚做的优化撤销,会造成一定的延时
1 | const { performance, PerformanceObserver } = require('perf_hooks') |
如果想进一步了解 V8 做了什么优化,可以利用 Node 的两个参数(trace-opt
、trace-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 | /* test.js */ |
对象优化
做这些优化的目的:迎合 V8 引擎进行解析,把你的代码进行优化。因为它也是用代码写的,所做的优化其实也是代码实现的规则,如果我们的代码迎合了这些规则,就可以帮你去做优化,代码效率可以得到提升
- 以相同顺序初始化对象成员,避免隐藏类的调整
- 实例化后避免添加新属性
- 尽量使用 Array 代替 array-like 对象
- 避免读取超过数组的长度
- 避免元素类型转换
以相同顺序初始化对象成员,避免隐藏类的调整
JS 是动态、弱类型语言,写的时候不会声明和强调它变量的类型,但是对于编辑器而言,实际上还是需要知道确定的类型,在解析时,它根据自己的推断,会给这些变量赋一个具体的类型,通常管这些类型叫隐藏类型(hidden class),之后所做的优化都是基于隐藏类型进行的
- 隐藏类型底层会以描述的数组进行存储,数组里会去强调所有属性声明的顺序,或者说索引,索引的位置
1 | class RectArea { // HC0 |
实例化后避免添加新属性
1 | // In-Object属性 |
尽量使用 Array 代替 array-like 对象
array-like
对象:JS 里都有一个 arguments 对象,包含了函数参数变量的信息,本身是一个对象,但是可以通过索引去访问里面的属性,它还有 length 属性,像是一个数组,但又不是数组,不具备数组上面的方法,比如:forEach- V8 引擎会对数组能极大性能优化,目前有 21 种不同的元素类型,最好是把类数组转成数组再进行遍历,这样会比不去转成数组直接遍历效率高
1 | // 不如在真实数组上效率高 |
避免读取超过数组的长度
arr[3] -> undefined
会与数字进行比较(JS 里越界不一定会报错)- 如果在数组对象里找不到,会沿着原型链向上找,所以会进行额外的开销
- 越界比较会造成原型链额外的查找,性能相差 6 倍
1 | function foo(arr) { |
避免元素类型转换
对于编辑器而言,实际上是有类型的
JavaScript 是不区分整数、浮点数和双精度,它们都是数字,但是在编辑器里会对这个做出精确的区分,如果使数组里面类型发生变化,就会造成额外的开销,效率就不高了
1 | const arr = [3, 2, 1] // PACKED_SMI_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
告诉浏览器,盒子里所有的子元素和盒子外面的元素之间没有任何布局上的关系。这样浏览器就可以对盒子里面的元素进行单独处理,不需要管理页面上其他的部分,这样就可以大大减少回流计算