关于前端:小程序编译器性能优化之路

7次阅读

共计 6620 个字符,预计需要花费 17 分钟才能阅读完成。

作者 | 马可

导读

小程序编译器是百度开发者工具中的编译构建模块,用来将小程序代码转换成运行时代码。旧版编译器因为业务倒退,存在编译慢、内存占用高的问题,咱们对编译器做了一次大规模的重构,采纳自研架构,做了多线程、代码缓存、sourcemap 等多项优化,在性能和内存占用上都有很大晋升。全文介绍了新版编译器的设计思路和优化办法,以及一些可能用在通用打包工具里的技术点。

全文 6629 字,预计浏览工夫 17 分钟。

01 前言

小程序编译器在小程序开发、预览、公布各个阶段都须要应用,因而编译器性能会间接影响到开发者开发效率,也会影响到开发者工具的应用体验。

因为旧版的编译器(基于 webpack4)在构建大型项目时会很慢,内存占用也高,始终被开发者吐槽。咱们通过大量的调研和开发,最初采纳齐全自研架构做新编译,针对小程序我的项目构建做了大量优化,根本解决了旧编译存在的问题。

下图是局部我的项目构建工夫比照:

新版编译器绝对于旧版实现了 2~7 倍的性能晋升,并且反对实时编译、热重载等个性,内存占用更少,构建产物更优。

上面从 框架选型、新编译器工作原理、性能和产物优化办法 等方面介绍新版编译器的成长之路。

02 框架选型

在进行新版编译器设计时,须要明确以后的痛点问题:性能,优先解决性能问题。其余新技术和新想法对编译器有帮忙的也一起施行。

旧版编译器基于 webpack4 存在如下几个问题:

  • 大型项目构建速度太慢。
  • dev 启动慢、增量编译慢,仅反对 loader 缓存,bundle 无缓存也比较慢。
  • 基于 webpack4 做扩大开发,须要 patch 局部模块能力工作,保护艰难。
  • 局部 webpack bundle 过程无奈针对小程序代码构造进行优化,存在有效构建。

新编译的设计指标:

  • 更快的全量编译速度,打消 webpack 存在的有效构建过程。
  • 反对全缓存,放慢首次和增量编译速度。
  • 反对实时编译,缩小 dev 启动和二次编译工夫。
  • 反对多线程编译减速,反对页面热重载。
  • 优化产物构造,缩小产物体积。

2.1 支流构建工具

上面介绍的是咱们调研过的支流前端构建工具,每个工具都有实用场景和优缺点。

在新版本编译器架构设计时,其余构建工具的设计理念和技术特点都值得参考。

Webpack 构建过程:

Webpack 长处:功能完善、社区沉闷、可配置性强、有很强的扩展性。

Webpack 毛病:配置简单、构建速度慢,二次开发艰难。

Parcel 构建过程:

Parcel 长处:无需配置,构建速度快,原生反对多线程和全缓存,多线程之间共享数据通过 lmdb 进行,防止跨线程通信开销。

Parcel 毛病:生态小,自定义性无限,大量采纳 Node 插件,兼容性也差一些。

Vite 构建过程:

Vite 长处:配置较为简单,按需编译,启动快,dev 时有不错的体验。

Vite 毛病:生态小,dev 和 公布走两套构建流程。

其余小程序平台:

  • 微信基于 gulp 和 C++ 模块做小程序构建,并且对 npm 模块做了预构建,在性能和开发体验上做的比拟好。
  • 支付宝基于 webpack 做小程序构建,并且应用了 esbuild 减速代码压缩。
  • 抖音小程序应用自研编译器,构建流程比较简单。

2.2 新版编译器

在设计新编译框架时,借鉴了支流打包工具的工作流程,联合小程序代码特点,决定不做通用打包工具,重点优化小程序打包性能。

最终抉择了自研编译器的计划,并做了大量优化工作,新版编译器优化点有如下几个方面:

1. 反对多 Compiler 协同工作,将动静库开发等多类型我的项目构建解耦。

2. 编译阶段全流程缓存,节俭二次构建工夫 90% 以上。

3.dev 开发默认采纳按需编译,晋升单页编译性能。

4. 反对 babel 和 swc 多线程编译,晋升全量编译速度 2 ~ 7 倍。

5. 采纳新版 sourcemap 协定,移除非必要解析合并,将 bundle 阶段耗时大幅缩减。

6. 对 js、css、swan 模板编译均做了构建时标记优化,缩小 bundle 合并耗时。

7. 对于预览、公布阶段的 js 压缩和混同,采纳了 terser 和 esbuild 并行计划,esbuild 用于疾速打出预览包,terser 能够保障压缩率用于公布包。

从后果看,新编译器从速度、资源占用和可维护性上绝对于旧版都有显著的晋升。

03 新版编译器工作原理

新编译器的解决流程和 parcel 比拟相似,Compiler 管制解决流程,Processor 进行代码转换,根本流程如下:

其中几个重要的模块:

  • CompileEntry 编译器为入口模块,蕴含 cli 通信、dev server 通信、命令调用等。
  • CompileManager 为编译管理器,用于依赖资源下载和治理以及多个 Compiler 协同构建。
  • Compiler 为编译器模块,用于将我的项目源码编译成运行时代码,我的项目构建时 Compiler 可能有多个。
  • Processor 为单元处理器,用于解决 代码转换、代码合并 等单个编译工作。

:小程序 App 我的项目有 1 个 Compiler,动静库和动静扩大我的项目 2 个 Compiler。

3.1 Compiler 编译器

用于编译单个小程序我的项目,将开发者原始代码编译为可运行代码。

工作职能:

1. 创立运行上下文,提供 config、fs 文件解决、watcher 监控、logger 等模块,给 Processor 应用。

2. 全量编译、文件变更时二次编译;这里二次编译也是走一遍全量编译流程,不过大部分用的是缓存后果。

3. 治理、调度、运行 Processor 处理单元。

4. 保护 Processor 依赖关系和后果缓存。

特点:

1. 实现全流程缓存,将每个 Processor 的输出参数、输入后果写入缓存,在有缓存状况下二次编译时长可缩小 90%。

2. 反对按需编译,每次按需单页编译、增量编译、全量编译 都走同样的 Processor 解决流程。

3. 通过 Proxy 机制主动计算缓存参数依赖,不必手动为每个 Processor 生成缓存 hash,绝对于 webpack 或 parcel 缩小 bug 产生。

4. 仅保护 Processor 依赖关系,不保护 ModuleGraph,简化解决流程。

对于全流程缓存每家打包器都有本人的实现计划,基本原理是依据以后输出参数和依赖状况为处理单元生成一个惟一 hash,hash 统一则后果统一。

webpack 和 parcel 因为保护了 ModuleGraph,缓存的计算和重用会简单一些。小程序编译器仅依据 Processor 入参和调用依赖进行计算。

3.2 Processor 单元处理器

Processor 有如下个性:

1. 在输出参数统一的状况下,保障输入统一,输出和输入都必须可序列化为 json,实现了 Processor 全缓存。

2.Processor 中的 uri 为构建 ID,在单次构建过程中 ID 统一则处理结果统一,例如解决 app.js 文件,uri 为:js:app.js,益处是能够对立 Processor 资源解决门路。

3.Processor 之间反对相互调用:processWith 调用并继续执行,processWithResult 调用并期待返回后果。

留神:这里的输出参数蕴含 uri、app config, contextFreeData。

几种罕用的 Processor:

1.JS Processor 将 es6 代码转换成 es5 代码,这是最耗时的模块。

2.Swan Processor 将 swan 模板代码转换成 view 层 js 代码。

3.Css Processor 应用 postcss 解决 css 中的单位转换、依赖收集等工作。

4.Bundle Processor 将后面 transformer 处理结果依照 bundle 算法合并文件并输入后果。

Processor 工作流程:

Processor 解决流程须要通过 transform -> bundle 的过程,在小程序里 js, css, swan 模板的 bundle 能够离开并行处理,这里和 webpack 的解决模式不一样,和 parcel 的 pipeline 相似。

3.3 性能和产物优化办法

3.3.1 多外围编译优化

因为 Node 中多线程模块初始化速度和通信效率比多过程好一些,新编译抉择应用 多线程 做多外围优化。

多线程编译有 2 种计划抉择:

  • 计划 1:基于 processor 做多线程调度,因为 processor 间反对互相调用,理论解决会很简单且有通信老本。
  • 旧的编译器做过基于 webpack 的 workerthread-loader,性能晋升无限(10%~15%)。
  • parcel 基于 lmdb 公共缓存打消线程间通信,保障读写效率,是一个比拟好的解决办法。
  • 计划 2:仅对 js 转译做多线程调度,仅有一来一回 2 次通信老本。
  • 应用 jest-worker 和 babel transform 做 js 多线程转译或者用 swc 多线程做 js 转译。

因为大部分构建工夫在 js 转译这里(js 中有大量 node\_modules 依赖,均须要转换),css 和 swan 模块转换耗时少。

最终抉择计划 2 仅做 js 多线程转译,解决流程简略且收益较好,整体晋升如下:

  • 应用 jest-worker 多线程 babel 转译,4 线程可晋升 1 倍以上速度。
  • 应用 swc 做 js 转译,4 线程晋升 4 倍以上速度。

JS Processor 多线程解决:

其中:

uri:为处理器构建 ID

contextFreeData:单次构建中不可变数据,例如 app.json 中的配置项

context args:全局参数,例如优化试验开关、多线程开关等

在 js 转换解决时规定了 transformer 对立转换接口,基于接口实现了 babel 单线程、babel 多线程、swc 转换 3 种处理器,并且可随时做处理器切换。

对于不同的编译环境能够做到灵便设置:

1. 开发者工具中开发者依据机器配置状况能够切换 多线程、swc 编译模式,晋升效率。

2. 云编译流水线默认开多线程编译进步性能。

3.webIDE 默认开单线程升高资源耗费。

3.3.2 SWC 编译优化

新编译器多线程模式绝对于旧编译晋升了 1 倍左右,在 dev 开发时一些大型项目页面首次编译还是有些慢,须要 10 秒以上,次要耗时在 js transform 这里。

swc 目前在 js 转译上根本成熟了,且大部分场景能晋升 4 倍以上转译速度,因而减少了 swc 多线程转译反对,将大型项目页面首次编译管制在了 5 秒以内。

须要编写 2 个 swc 插件来适配 swc 转译:

  • @swanide/swc-require-rename 将 require/import/export 中的模块提取门路信息,以便于后续在 js 中剖析模块依赖关系。
  • @swanide/swc-web-debug 对 js 代码进行插桩解决,用来反对真机调试中的断点调试。

swc 编译带来的性能晋升是微小的,在应用中也发现了一些问题:

1.swc 存在内存泄露,在 dev 阶段如果全量编译次数过多,会导致内存占用很高,需手动重启编译器。

2.swc 插件反对的 api 较少,一部分 babel 容易实现的性能,在 swc 中很难解决。

3.swc 因为应用 rust 编写插件,插件在不同 @swc/core 版本间不能通用,须要为不同平台生成 swc 插件,在部署上会麻烦一些。

在理论应用中,对于一部分 swc 不能很好解决的场景,会降级到 babel 解决。

3.3.3 代码压缩和运行时缓存

在 dev 阶段,编译后的代码是没有通过压缩的,能够在模拟器中运行。在预览公布阶段因为限度了包体积,须要做代码压缩以缩小产物体积。

可选的代码压缩工具有如下 3 个:

1.terser 压缩率高,产物体积小,速度最慢。

2.swc 压缩快,mangle 反对不欠缺,压缩率较差。

3.esbuild 压缩最快(比 terser 快了 10 倍以上),反对 mangle,代码压缩率不如 terser。

最初通过比照思考,抉择了如下压缩计划:

1. 预览阶段因为不须要 sourcemap,移除 sourcemap,并应用 esbuild 做代码压缩,进步预览速度(对于主动预览场景有很大晋升)。

2. 公布阶段应用 terser 做多线程压缩,并保留 sourcemap。

运行时缓存 指的是构建过程的两头后果都在内存中做了缓存,包含 Processor 处理结果 和 代码压缩后果,在二次构建时能够节俭大部分从新构建工夫。因为缓存中保留的是字符串和 json 对象,绝对于基于 webpack 的旧版编译器有 40% ~ 60% 的内存节俭,在内存占用上处于可承受范畴。

3.3.4 Swan 模板解决优化

旧的 swan 模板解决应用 swan-loader 进行模板转换,因为设计时没有解决好模板 import 作用域,导致 <template> 标签以及 filter 过滤器函数只能内联到页面代码中,如果模板中大量应用了 template 和 filter,最终生成的代码体积会十分大。

新编编译器纠正了 import 作用域关系,将编译产物中的 template、filter 生成模式由内联改为 require 援用,而后在 bundle 阶段做代码合并,使雷同模块可能失去重用,算是填了一个大坑。

新编译器 swan 模板解决流程:

单个 swan 文件通过 Processor 解决后可能的产物有:

  • component 组件模块,用于生成页面和自定义组件
  • template 模块
  • filter 过滤器函数、sjs 过滤器函数
  • transformed document 中间代码

将 swan 模板转换成不同类型的 js module,并保护依赖关系,便于后续的代码合并时更精细化的管制。

因为历史起因 import/include 中蕴含 sjs 或者 template 援用时不能间接生成 template 模块,须要在最初入口模板中生成。新编译也提供了 template 动态编译选项,将严格限度 import 作用域,可间接生成 template 模块代码,对于 taro 生成的小程序我的项目能够节约 30% 左右的产物大小。

3.3.5 Sourcemap 优化

因为编译器须要反对 js 代码调试以及运行时 error 跟踪,在 dev 和公布阶段都须要生成 sourcemap。

在 webpack 中生成代码时须要对 sourcemap 进行合并计算,较大的我的项目 sourcemap 合并会占用很长时间,并且每次从新编译都要从新计算 sourcemap。

调研时发现浏览器 devtools 对 sourcemap 协定 的 index map 反对十分好,新编译器基于 index map 协定做了 sourcemap 合并优化,由之前的多文件 sourcemap 合并计算,变成了计算生成 offset map 并拼接内容,这样 js bundle 耗时就由原来的 几秒到几十秒变为了固定 3 秒以内。

一个有意思的事件是 vscode 的 js-debugger 直到 22 年 6 月份才反对 index map 调试(index map 2011 年公布的),微软的动作略微慢了一些。

3.3.6 后续工作

在新编译器开发实现之后的推广中,采纳了渐进式推广形式:

第一阶段,开发者工具新旧编译器共存,dev、预览应用新编译器,公布应用旧编译器。

第二阶段,外部 pipeline 预览和公布全量应用新编译。

第三阶段,开发者工具全副切换到新编译器。

新版编译实际上线后还存在一些小的兼容性问题,须要尽量提前裸露问题能力做公布全量替换。

针对小程序我的项目,新编译做了大量的优化工作,局部优化工作还没有实现开发,包含:

hmr 热重载:开发中,因为 运行时框架、开发者工具均须要做接口适配,须要较长时间调试能力达到预期。

tree-shaking 代码打消:对于 es6 模块在 transform 阶段能够做 tree-shaking 消减代码。

scope-hoisting 作用域晋升:实践可行,须要验证代码缩减成果。

新版编译器因为须要齐全兼容旧版编译器构建后果,在 bundle 打包场景还存在优化空间,咱们在后续工作中配合运行时框架能够做更多打包产物优化。

04 总结

新版编译器采纳自研打包计划,比照基于 webpack 的旧编译器实现了微小的性能晋升,彻底解决了编译慢、资源占用高的问题,绝对友商的编译器也有不错的性能劣势。

一些新编译引入的优化伎俩如 swc 转译、esbuild 压缩、sourcemap 优化 也能用在其余前端我的项目构建中,并起到减速成果。

在新编译器我的项目中每个同学都十分致力,奉献了很多微妙的点子,遇到的大部分难题都无效解决了。咱们会持续保持性能和产物优化这两个方向,一直晋升开发者体验和运行时效率。

——END——

举荐浏览

百度 APP iOS 端包体积 50M 优化实际 (六) 无用办法清理

基于异样上线场景的实时拦挡与问题散发策略

极致优化 SSD 并行读调度

AI 文本创作在百度 App 发文的实际

DeeTune:基于 eBPF 的百度网络框架设计与利用

正文完
 0