likes
comments
collection
share

了解JS静态分析,打开前端优化新思路

作者站长头像
站长
· 阅读数 14

前言

我们经常能在webpack和ES6相关知识中看到静态分析(静态优化)这个词,今天我们就来给静态分析做个分析,希望看完这篇文章,能让你知道以下三个内容:

  1. 什么是静态分析?
  2. 为什么要做静态分析?
  3. JS如何做静态分析?

1. 什么是静态分析

静态分析通俗来讲就是在程序没有运行的时候,对它的语法、词法等进行代码扫描分析,发现可能存在的问题,并针对问题做优化改进。 对于JS来讲,eslint tree-shaking 可以说是我们经常听到的静态代码分析工具了,本篇也就是重点介绍这两个工具是如何对代码做静态分析和优化的。

2.为什么要做静态分析?

设想一下,如果我们写的代码没有经过工具的检测就放到服务器使用,出现错误的概率几乎是100%!再设想一下,一个项目经过年复一年的开发迭代,经过多少的人,多少的变更,里面有多少不被使用却依然被保留,或者引入了却没被调用的代码,要解决这些问题就需要做静态分析。

3.JS如何做静态分析?

接下来是本篇文章的重点,我们将介绍eslinttree-shaking是如何做静态分析的,在开始前,我们就不得不先了解下JS是怎么运行的。

3.1 JS运行过程

我们都知道JS可以在浏览器中运行,我们就拿Chrome的V8引擎来讲JS的执行过程,在这之前先了解几个概念:

  • 编译器(Compiler):将源代码在运行之前编译成计算机能执行的机器码,由于要编译完所有源代码后在执行,所以编译器需要更多的内存存储机器码,但执行快;
  • 解释器(Interpreter):将源代码在运行时逐行解释执行,由于是一边解释一边执行,故启动快,执行慢;
  • 抽象语法树(AST):解析器(Parser)将源代码进行词法分析、语法分析后生成的抽象语法树,想要看生成的结果请戳:astexplorer.net/
  • 字节码(Bytecode):又称作中间代码,在JS解析中就是从AST -> 字节码 -> 机器码,字节码是后面才被V8引擎引入的,主要目的是为了解决机器码带来的内存占用问题;
  • 即时编译器(JIT):简单的理解就是一段代码被解释器执行多次之后就会变成热点代码(HotSpot),热点代码会被编译器直接编译成机器码,当代码再次执行时直接运行机器码,从而达到提高性能的目的,这种编译器和解释器混合使用的技术被叫做即时编译。

了解了几个基本概念后我们再来看看V8执行一段JS代码的过程图:

了解JS静态分析,打开前端优化新思路

关于即时编译器的运行过程可以看下图:

了解JS静态分析,打开前端优化新思路

(图片来自极客时间 --《浏览器工作原理与实践》)

有同学想要再深入了解运行原理可以看看浏览器工作原理与实践 课程(要钱)或者看看其他文章或官网

3.2 JS的静态分析阶段

从上面的图中可以看出执行一段JS代码需要经历3次重要的代码转换即:源码->AST->字节码->机器码,最常见的静态分析阶段就是在 源码->AST 这个过程,比如eslint,tree-shaking,babel,uglify等都是把源码转换成AST后去做分析处理的。

3.3 eslint静态分析

这里我们来实际跑一跑eslint的检验过程,看一看它是怎么把源码转换成AST后去做分析处理的。

Step1:拉仓库

拉取源码,安装完毕后,我们选一条比较简单的规则 lib/rules/default-case.js 来看效果,本条规则是对switch语句是否有default做检测。

Step2:准备调试环境

我们使用vscode去跑eslint的规则测试,eslint单测用的是mocha,配置vscode调试如下:

{
    // 在.vscode目录下的launch.json文件
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Run mocha",
            "type": "node",
            "request": "launch",
            "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", //启用mocha脚本
            "stopOnEntry": false,
            "args": [
                "tests/lib/rules/default-case.js", //这个就是我们等下要调试的规则
                "--no-timeouts"
            ],
            "cwd": "${workspaceRoot}",
            "runtimeExecutable": null,
            "env": {
                "NODE_ENV": "testing"
            }
        }
    ]
}
Step3:跑起来

前置知识:关于eslint的规则怎么写,AST节点类型介绍都可以看我们的另一篇文章深入浅出之ESLint

看下图的4个重要文件: 了解JS静态分析,打开前端优化新思路

调试进入linter.js文件可以看到AST的转化结果: 了解JS静态分析,打开前端优化新思路

如原始语句:switch (a) { case 1: break; default: break; } 先转化成AST如下:

{
    "type": "Program",
    "loc": {
        "start": {
            "line": 1,
            "column": 0
        },
        "end": {
            "line": 1,
            "column": 45
        }
    },
    "range": [0, 45],
    "body": [{
        "type": "SwitchStatement", //这个就是要判断的switch节点
        "loc": {
            "start": {
                "line": 1,
                "column": 0
            },
            "end": {
                "line": 1,
                "column": 45
            }
        },
        "range": [0, 45],
        "discriminant": {
            "type": "Identifier",
            "loc": {
                "start": {
                    "line": 1,
                    "column": 8
                },
                "end": {
                    "line": 1,
                    "column": 9
                }
            },
            "range": [8, 9],
            "name": "a"
        },
        "cases": [{
            "type": "SwitchCase",
            "loc": {
                "start": {
                    "line": 1,
                    "column": 13
                },
                "end": {
                    "line": 1,
                    "column": 27
                }
            },
            "range": [13, 27],
            "consequent": [{
                "type": "BreakStatement",
                "loc": {
                    "start": {
                        "line": 1,
                        "column": 21
                    },
                    "end": {
                        "line": 1,
                        "column": 27
                    }
                },
                "range": [21, 27],
                "label": null
            }],
            "test": {
                "type": "Literal",
                "loc": {
                    "start": {
                        "line": 1,
                        "column": 18
                    },
                    "end": {
                        "line": 1,
                        "column": 19
                    }
                },
                "range": [18, 19],
                "value": 1,
                "raw": "1"
            }
        }, {
            "type": "SwitchCase",
            "loc": {
                "start": {
                    "line": 1,
                    "column": 28
                },
                "end": {
                    "line": 1,
                    "column": 43
                }
            },
            "range": [28, 43],
            "consequent": [{
                "type": "BreakStatement",
                "loc": {
                    "start": {
                        "line": 1,
                        "column": 37
                    },
                    "end": {
                        "line": 1,
                        "column": 43
                    }
                },
                "range": [37, 43],
                "label": null
            }],
            "test": null
        }]
    }],
    "sourceType": "script",
    "comments": [],
    "tokens": [{
        "type": "Keyword",
        "value": "switch",
        "loc": {
            "start": {
                "line": 1,
                "column": 0
            },
            "end": {
                "line": 1,
                "column": 6
            }
        },
        "range": [0, 6]
    }, {
        "type": "Punctuator",
        "value": "(",
        "loc": {
            "start": {
                "line": 1,
                "column": 7
            },
            "end": {
                "line": 1,
                "column": 8
            }
        },
        "range": [7, 8]
    }, {
        "type": "Identifier",
        "value": "a",
        "loc": {
            "start": {
                "line": 1,
                "column": 8
            },
            "end": {
                "line": 1,
                "column": 9
            }
        },
        "range": [8, 9]
    }, {
        "type": "Punctuator",
        "value": ")",
        "loc": {
            "start": {
                "line": 1,
                "column": 9
            },
            "end": {
                "line": 1,
                "column": 10
            }
        },
        "range": [9, 10]
    }, {
        "type": "Punctuator",
        "value": "{",
        "loc": {
            "start": {
                "line": 1,
                "column": 11
            },
            "end": {
                "line": 1,
                "column": 12
            }
        },
        "range": [11, 12]
    }, {
        "type": "Keyword",
        "value": "case",
        "loc": {
            "start": {
                "line": 1,
                "column": 13
            },
            "end": {
                "line": 1,
                "column": 17
            }
        },
        "range": [13, 17]
    }, {
        "type": "Numeric",
        "value": "1",
        "loc": {
            "start": {
                "line": 1,
                "column": 18
            },
            "end": {
                "line": 1,
                "column": 19
            }
        },
        "range": [18, 19]
    }, {
        "type": "Punctuator",
        "value": ":",
        "loc": {
            "start": {
                "line": 1,
                "column": 19
            },
            "end": {
                "line": 1,
                "column": 20
            }
        },
        "range": [19, 20]
    }, {
        "type": "Keyword",
        "value": "break",
        "loc": {
            "start": {
                "line": 1,
                "column": 21
            },
            "end": {
                "line": 1,
                "column": 26
            }
        },
        "range": [21, 26]
    }, {
        "type": "Punctuator",
        "value": ";",
        "loc": {
            "start": {
                "line": 1,
                "column": 26
            },
            "end": {
                "line": 1,
                "column": 27
            }
        },
        "range": [26, 27]
    }, {
        "type": "Keyword",
        "value": "default",
        "loc": {
            "start": {
                "line": 1,
                "column": 28
            },
            "end": {
                "line": 1,
                "column": 35
            }
        },
        "range": [28, 35]
    }, {
        "type": "Punctuator",
        "value": ":",
        "loc": {
            "start": {
                "line": 1,
                "column": 35
            },
            "end": {
                "line": 1,
                "column": 36
            }
        },
        "range": [35, 36]
    }, {
        "type": "Keyword",
        "value": "break",
        "loc": {
            "start": {
                "line": 1,
                "column": 37
            },
            "end": {
                "line": 1,
                "column": 42
            }
        },
        "range": [37, 42]
    }, {
        "type": "Punctuator",
        "value": ";",
        "loc": {
            "start": {
                "line": 1,
                "column": 42
            },
            "end": {
                "line": 1,
                "column": 43
            }
        },
        "range": [42, 43]
    }, {
        "type": "Punctuator",
        "value": "}",
        "loc": {
            "start": {
                "line": 1,
                "column": 44
            },
            "end": {
                "line": 1,
                "column": 45
            }
        },
        "range": [44, 45]
    }]
}

内容很长,但是需要处理匹配的节点只有SwitchStatement,到lib/rules/default-case.js中去查看运行结果: 了解JS静态分析,打开前端优化新思路 符合规则则通过,否则不通过。

小结:到这里就已经跑完eslint校验的整个过程,看起来很简单,里面的工程却是很大,在此不详细讲里面的细节,这里主要就是让大家了解它是怎么把 源码 转 AST 后去做分析处理的,感兴趣的同学可以自己去拉源码跑一跑。

3.4 tree-shaking静态优化

tree-shaking中文摇树,意思就是把挂树上没用的东西(模块)摇掉。在此之前,我们需要先知道:tree-shaking的模块消除原理是基于ES6的模块特性。

阮一峰老师说的:ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJSAMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。【Module 的语法】

所以只有在ESM的模块设计之下tree-shaking才有它的用武之地。和传统的代码优化工具如uglify不同,tree-shaking关注于消除没有用到的代码,而uglify关注于消除不会执行的代码。

来看看tree-shaking的效果

如下演示效果是用webpack举例,webpack2.0之后已引入tree-shaking,开启tree-shaking方式需要满足:

  1. 使用 ESM 规范编写模块代码
  2. 启用optimization代码优化功能

简单写个变量导入: 了解JS静态分析,打开前端优化新思路

打包后的结果,成功把没用到的test变量干掉了解JS静态分析,打开前端优化新思路

如上可以看出模块的引入确实可以得到优化,但是许多代码副作用带来的问题,使Tree Shaking的优化效果并不能达到预期,比如下面这番无意义的赋值操作:

了解JS静态分析,打开前端优化新思路 并没有把test变量干掉: 了解JS静态分析,打开前端优化新思路

所以前端的优化靠工具是完全不够的,还需要靠智人们(聪明的你们)良好的编码习惯和规范,方能打造出人见人爱,花见花开的优秀代码。

顺带验证一下上面说的如果用CommonJS的方式引入则没法做到tree-shaking的效果: 了解JS静态分析,打开前端优化新思路

恩!果不其然: 了解JS静态分析,打开前端优化新思路

有想要自己玩玩的可以拉我的demo项目快速跑一跑:webpack-demo

tree-shaking的静态优化就解释到这里,看官想要继续深入理解里面的原理可以看看【Tree-Shaking性能优化实践 - 原理篇】 【Webpack 原理系列九】这里不多做赘述。

总结

本篇目的就是让大家了解什么是静态分析,前端是如何做到静态分析并且有哪些比较常见的方式,当下次你再看到关于静态分析静态优化亦或者静态化时,能知道他们想要说的是什么。

前端静态优化的路还很长,TypeScript的崛起、ES6模块的静态化设计、越来越多的分析工具,未来还会有更多的新科技新技能出现,向为我们创造更好的IT时代的巨人们致敬。