性能优化之代码优化

本文最后更新于:2021/07/05 , 星期一 , 21:32

JS开销和如何缩短解析时间

JS开销

js时间开销

解决方案

  1. 网络加载方面:JS文件压缩

  2. 编译和解析:代码拆分,按需加载

  3. 编译和解析:删除没必要的代码

  4. 执行:减少主线程工作量

减少主线程工作量

  1. 避免长任务(long task)

  2. 避免超过1kB的行间脚本

  3. 使用 rAF 和 rIC 进行时间调度

配合 V8 有效优化代码

V8编译原理

  1. 生成抽象语法树(AST)和执行上下文:先进行词法分析将源代码拆解成token。在进行语法分析,将token根据语法规则转化为AST。

  2. 生成字节码:会根据 AST 生成字节码,并解释执行字节码

  3. 执行代码:在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器就会把该段热点的字节码编译为机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了。经过编译器优化过的代码只能针对某种固定的结构,一旦在执行过程中,对象的结构被动态修改了,那么优化之后的代码会变成无效的代码,这时候优化编译器就需要执行反优化操作,经过反优化的代码,下次执行时就会回退到解释器解释执行。

eg.:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const {performance,PerformanceObserver} = require('perf_hooks);

const add = (a,b) => a + b;

const num1 = 1;
const numb = 2;

performance.mark('start);
for(let i = 0 ; i < 100000 ; i ++){
add(num1,num2)
}
//分别注释拼接字符串的代码进行时间比较,发现时间差距极大。
add(num1,'s');

for(let i = 0 ; i < 100000 ; i ++){
add(num1,num2)
}

performance.mark('end');

const observer = new PerformanceObserver((list)=>{
console.log(list.getEntries()[0]);
})

查看V8对哪些进行了优化,哪些进行了反优化可以执行node --trace-opt --trace-deopt file.js

V8优化机制

  1. 脚本流:下载的过程中对已经下载的足够大的js文件开辟新线程先解析,等都加载完后将全部解析结果合并。

  2. 字节码缓存

  3. 懒解析(lazy-parsing):主要对于函数而言,先不解析函数内部逻辑,等用的时候在解析。

函数优化

函数的解析方式

  • 懒解析(lazy-parsing):不会创建ast,会创建作用域,但不会在里面包含变量引用或者声明。

  • 饥饿解析(eager-parsing):会建立AST,创建完整的作用域,找出所有语法错误

问题:一个马上要执行的函数在声明的时候会进行一个懒解析,发现是该函数要执行了,又会进行饥饿解析,效率降低。

eg.:

1
2
3
4
5
6
7
8
const main = ()=>{
//可通过将函数转为立即执行函数,告诉V8,add进行饥饿解析。
// const add =((a,b)=>a+b);
const add = (a,b)=>a+b;
const num1 = 1;
const num2 = 2;
add(num1 + num2);
}

问题:使用部分压缩工具(老版本的uglyfy 和 webpack等)时,可能会将上述括号取消掉。
解决:利用Optimize.js优化加载时间。(将括号再加回来)

对象优化

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

隐藏类可以理解为“按图索骥”中的图。提高对象属性的访问速度,快速存取对象属性,节省内存空间。

详细可看 V8 中的对象表示

文章 JavaScript 引擎基础:Shapes 和 Inline Caches 中详细了隐藏类的相关内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class RectArea{//HC0
constructor(l,w){
this.l = l;//HC1
this.w = w;//HC2
}
}

//这种实例化时,rect2会复用rect1时产生的HC(hidden class)。
const rect1 = new RectArea(3,4);
const rect2 = new RectArea(5,6);

//反面例子
const car1 = {color:'red'}; //HC0
car1.seats = 4; //HC1

const car2 = {seats:2}; //无法复用以上两种,创建新的HC2
cat2.color = 'blue'; //HC3

2. 实例化后避免添加新属性

1
2
const car1 = {color:'red'}; // In-object属性
car1.seats = 4; //Normal/Fast属性,存储在property store里,需要通过描述数组简介查找

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

array-like对象:形如函数参数(arguments这种,有索引有length的对象)

1
2
3
4
5
6
7
8
9
Array.prototype.forEach.call(arrObj,(v,i)=>{ //不如在真实数组上效率高。
console.log(`${v}:${i}`)
})

//正确姿势
const arr = Array.prototype.slice.call(arrObj,0);//转换的代价比优化影响小
arr.forEach((v,i)=>{ //不如在真实数组上效率高。
console.log(`${v}:${i}`)
})

4. 避免读取超过数组的长度

1
2
3
4
5
6
7
function foo(arr){
for(let i = 0 ; i <= arr.length ; i++){ // 取值越界
if(array[i]>1000){ //1. undefined和1000 进行比较 2.沿原型链查找,造成额外的插销。
console.log(arr[i]);// 业务上无效
}
}
}

5. 避免元素类型转换

1
2
const array = [3,2,1]; //类型:PACKED_SMI_ELEMENTS:满的_smallInt
array.push(4.4); //上述类型失效,类型变为:PACKED_DOUBLE_ELEMENTS

避免 -0,除非你需要在代码中明确区分 -0 和 +0。

同样还有 NaN 和 Infinity。它们被表示为双精度,因此添加一个 NaN 或 Infinity 会将 SMI_ELEMENTS 转换为DOUBLE_ELEMENTS。

元素类型转换

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

越具体优化越多,越通用优化越小。

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

6. 避免创建洞

1
2
3
4
5
6
7
8
9
const array = new Array(3);
// 此时,数组是稀疏的,所以它被标记为 `HOLEY_SMI_ELEMENTS`
array[0] = 'a';
// 接着,这是一个字符串,而不是一个小整数...所以过渡到`HOLEY_ELEMENTS`。
array[1] = 'b';
array[2] = 'c';
// 这时,数组中的所有三个位置都被填充,所以数组被打包(即不再稀疏)。
// 但是,我们无法转换为更具体的类型,例如 “PACKED_ELEMENTS”。
// 元素类保留为“HOLEY_ELEMENTS”。

解决方案

1
2
3
4
5
//方法1
let array = []
array.push(newElement) //循环
// 方法2
let array = [1,2,3,4,5]

7. 避免多态

如果代码需要处理包含多种不同元素类型的数组,则可能会比单个元素类型数组要慢,因为代码要对不同类型的数组元素进行多态操作。

1
2
3
4
5
6
7
8
9
10
11
12
const each = (array, callback) => {
for (let index = 0; index < array.length; ++index) {
const item = array[index];
callback(item);
}
};
const doSomething = (item) => console.log(item);


each([1, 2, 3], doSomething);
each([1.1, 2.2, 3.3], doSomething);
each(['a', 'b', 'c'], doSomething);

调用了each3次,并且每次都没有给它相同的元素类型,在V8中,它采用内联缓存(Inline Caches,简称 IC)来缓存调用的实现以优化这些操作的执行过程。
当第一次只传入类型为packed_smi_element[1,2,3],v8会使用IC来缓存这个方法的调用,记录元素类型以及其他信息,那么下一次传入packed_smi_element时,直接就可以从缓存里取到优化后的调用方法,然后进行调用。
但是第二次如果传入的不一样的元素类型,比如packed_double_number,那么v8又会重新缓存一个新的调用实现(适用于packed_double_number),那么传入元素的时候就需要进行2次判断了,先判断是不是smi,如果不是,就判断是不是packed_double_number,如果是其他,那么又会重新缓存一个新的调用实现.优化与反优化

HTML优化

  • 减少iframes使用。必要使用时,先建立个空iframe,延迟或在合适的时机使用setAttribute赋值src

  • 压缩空白符

  • 避免节点深层级嵌套

  • 避免table布局

  • 删除注释(和压缩空白符一个道理,压缩文件)

  • CSS&JavaSCript尽量外链(避免HTML大文件)

  • 标签语义化(1.方便人看懂代码 2.方便浏览器针对标签做的优化)

  • JS文件放到尾部(加载JS是阻塞的,会阻塞渲染)

CSS对性能的影响

利用DevTools测量样式开销

CSS优化

  • 降低CSS对渲染的阻塞(1.尽量早的去下载CSS 2.降低CSS文件大小,仅加载首屏需要的CSS,不需要的延迟加载)

  • 利用GPU进行完成动画(单独出一个层,不影响布局与绘制)

  • 使用contain属性(减少重绘与回流)

  • 使用font-display属性(让文字更早的显示在页面上,减轻字体闪动)


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!