什么网站可以自己做配图,wordpress 代码页面跳转,网站被攻击如何处理,广州商城建站系统作者 | 马可 导读 小程序编译器是百度开发者工具中的编译构建模块#xff0c;用来将小程序代码转换成运行时代码。旧版编译器由于业务发展#xff0c;存在编译慢、内存占用高的问题#xff0c;我们对编译器做了一次大规模的重构#xff0c;采用自研架构#xff0c;做了多线…
作者 | 马可 导读 小程序编译器是百度开发者工具中的编译构建模块用来将小程序代码转换成运行时代码。旧版编译器由于业务发展存在编译慢、内存占用高的问题我们对编译器做了一次大规模的重构采用自研架构做了多线程、代码缓存、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简化处理流程。
关于全流程缓存每家打包器都有自己的实现方案基本原理是根据当前输入参数和依赖情况为处理单元生成一个唯一 hashhash 一致则结果一致。
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 作用域导致 标签以及 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 的百度网络框架设计与应用