Redian新闻
>
Nodejs应用编译构建提速建议

Nodejs应用编译构建提速建议

公众号新闻

来源 | OSCHINA 社区

作者 | 京东云开发者-京东科技 林光辉

原文链接:https://my.oschina.net/u/4090830/blog/9093023

编译构建的整体过程

  1. 拉取编译镜像

  2. 拉取缓存镜像

  3. 拉取项目源码

  4. 挂载缓存目录

  5. 执行编译命令 (用户自定义)

  6. 持久化缓存

  7. 上传编译镜像

为什么在本地构建就快,但编译机上很慢

在编辑机上每次的构建环境都是全新的,完成一次构建比本地需要多一些步骤:

  1. 现成的全局包缓存 VS 重新构建缓存: 咱可以先简单理解为咱使用 npm 的时候那个全局的缓存目录,编辑机需要准备持久化的缓存的环境,包括下载、挂载以重建缓存,如果缓存内容过大,时间也会相对更长,本地构建直接使用了稳定的本地文件系统;

  2. 增量安装依赖 VS 全量安装依赖: 本地不太经常需要执行 install 的过程,即使需要,也因为有持久的 node_modules 目录存在,不需要全量安装,但编辑机环境每次需要重新安装这个项目需要的所有依赖;

  3. 增量构建 VS 全量构建: 本地构建默认会将构建缓存放到 node_modules 目录下,第二次构建的时候这些构建就能被用起来,使得后面的构建更快,但这个构建的默认缓存位置在编辑机上不会被持久化,也就是每次需要全量构建.

  4. 网络环境: 有些依赖包安装依赖外部网络甚至海外网络,本地的网络环境比较顺畅,但编辑机的网络对与海外网的访问没有保证.

  5. 难以利用的优势: 多核大内存,nodejs 项目的构建,大部分工作都在一个线程上执行了,不好直接利用编译机的多核优势

  6. 额外的步骤: 编译机需要下载镜像、制作并上传运行镜像、缓存内容持久化,而本地一般只是产出包.

所以从以上角度入手,我们可以基于这样的一些思路进行构建速度的优化:

  1. 优化镜像大小;

  2. 善用持久化缓存实现增量构建 (编辑机会对 /cache/ 目录下的内容进行持久缓存)

  3. 充分利用多核优势:

    比如 ts-loader 的类型校验就可以通过其它插件在单独的线程执行,eslint-loader 也支持多线程 (但目前有 bug, 不建议使用).

    再比如我们可以对项目的各功能模块解耦,拆成多个构建同时进行。

  4. 减少不必要的构建:

    比如合理配置 exclude 以精简构建文件范围;

    对于不常变动的文件,拆出来一次构建,下次复用.

  5. 判断是否可能有其它方式去掉对外网依赖的包

如何分析构建速度

  1. 检查 /cache/ 目录大小:

  2. 在编译命令中加入:du -sh /cache, 通过构建日志查看目录大小

  3. 在整体编译命令前后都加上 date, 可以看自己项目的构建过程耗时,即编译命令执行时间

  4. 在主要的编译命令的每一行前面加上 time, eg:time npm install 可以看 install 过程的实际耗时,build 过程同理.

  5. 对比整体构建时间 (网页上直接显示的任务时间) 与编译命令执行时间 (末尾的 date 时间 - 开头的 date 时间), 如果整体时间超过编译命令执行时间很多 (> 1min30s), 可能是 /cache/ 目录或镜像过大导致的。

以下为详情介绍:

使用更小的运行镜像

如果有较大的镜像,建议联系运维进行优化.

善用持久缓存

缓存可以对应用构建带来提速的效果,但如果缓存目录持续增长,大到一定程度反倒可能让速度变慢.

了解缓存机制:

1. 缓存目录: /cache/

2. 默认行为: 对于 nodejs 的应用, 目前持久缓存会为 npm, pnpm 提供安装包的缓存, 以加快 npm install / pnpm install 的过程

3. 工作原理:

3.1 /cache/ 目录下的内容会构建成功后自动上传到服务器进行存储, 并在下次构建任务执行前进行挂载

3.2 /cache/ 与 当前工作目录(即 './', 拉取的源码存放位置) 不在同一个文件系统(相当于是缓存在C盘而源码在D盘), pnpm install的行为将从 hark link回退为文件复制(硬链接的方式相对于大量小文件的拷贝, 速度要快很多)

3.3 /cache/ 的工作涉及上传、下载过程, 如果过大也将会影响整个构建过程的速度

排除全局缓存对构建速度的影响

检查 /cache/ 的大小,可以在编译命令中加入:du -sh /cache, 查看日志,如果文件夹超过 1G (仅供参考), 建议咚咚联系行云部署 (j-one) 对应用缓存进行清理

解决缓存跨盘造成的性能损失

主要思路: 使源码与 /cache/ 处于同一个文件系统。目前对于 pnpm 的应用推荐该方式.
原理: 使源码与 /cache/ 处于同一个文件系统,这可以让 pnpm 的 hard link 方式生效,相对于 node_modules 那些数以万计的小文件复制,执行效率会得到可观的提升。参考:Pnpm 是否可以跨多个驱动器或文件系统工作?
方式: 将当前工作目录的代码复制到 /cache/ 下再执行 install、build 命令.
参考命令:
    # 记下当前工作目录
CUR_WORKSPACE=`pwd`
# 存放源码
# 咱统一用 /cache/source 放源码就好, 虽然也可以改成其它目录的名字
mkdir -p /cache/source
# 拷贝当前目录的代码, 到 /cache/source 下
rsync -r ./ /cache/source --exclude=node_modules --exclude=.git
# 切换 workspace
cd /cache/source
########## 这里替换成自己需要的内容 ###########
# 执行 install
pnpm i
# 执行 build
pnpm run build
########## 这里替换成自己需要的内容 ###########

# 将构建结果拷贝到抽包地址

########## 如果不是 dist, 请根据需要换成其它目录, 就是你项目构建完生成的目标代码目录
cp -r ./dist/* ${CUR_WORKSPACE}/.build
# 删除不需要被缓存的文件
cd ../ && rm -rf /cache/source

以上编译命令基于行云部署前端项目本身精简
请大家在理解原理、思路的基础上根据自身需要修改.

缓存构建结果

webpack 及其插件,会对构建结果进行缓存。我们可以利用 /cache/ 的持久化缓存来实现代码构建缓存。其它构建工具也可以参考相关文档进行配置.
如果使用 webpack4 或依赖 webpack4 的构建工具,比如 @vue/cli-service 等,通常会使用 cache-loader 对构建结果进行缓存,babel-loader 也会有自己的构建缓存,但默认都放在 node_modules/.cache 目录下,建议参考相关文档将 cache 目录设置为 /cache/build (或者其它 /cache/ 的子目录)
对于 webpack5, 自己就已经集成了 cache 功能,可以删掉 cache-loader 等插件,减少不必要的工作。参考:webpack cache
如果是 monorepo 的应用,还可以实现子项目级别的缓存,比如使用 nx 进行 monorepo 的管理,则可以配置 NX_CACHE_DIRECTORY 来设置缓存地址,eg:
export NX_CACHE_DIRECTORY=/cache/jdos3-console-ui/.nx

eslint 也是一个很费时的操作,它也支持缓存,但默认不开启,如果有需要也可以开启缓存,但缓存策略需要使用 'content', 因为每次构建文件的 createTime 都会改变,metadata 的策略会失灵。参考:eslint cache
通常我们需要同时兼容本地开发和行云部署的构建,可以通过环境变量的方式实现,以 webpack5 为例:
webpack5 的缓存配置:
{
cache: {
type: 'filesystem',
profile: true,
cacheDirectory: process.env.BUILD_CACHE_DIRECTORY,
compression: 'gzip',
},
}

同时在行云部署的编译命令中增加:
export BUILD_CACHE_DIRECTORY=/cache/.webpack

另一种利用缓存的思路:缓存 node_modules

(编译团队提出了这种思路,我目前没有进行相关尝试,产品上针对该思路的通用解决方案在探索中)
主要思路: 模拟本地构建 (本地构建会持久保留 node_modules 目录)
收益:
1. 加速 install 的过程,减少包的安装.
2. 利用代码构建缓存: webpack5 或 babel-loader 等一般会在 node_modules/.cache 目录下存放构建缓存,这也是很多应用本地构建较快的原因。当然 .cache 目录会持续增长,需要定时清理,有兴趣大家可以看看本地的代码里是否有这个目录,占多大空间.
参考命令:
大体上与上面 ' 解决缓存跨盘造成的性能损失 ' 过程相同,只是最后 rm 的过程保留 node_modules 目录,以供下次使用
    ####### 与上面 解决缓存跨盘造成的性能损失 一致 #########
# 记下当前工作目录
CUR_WORKSPACE=`pwd`
# 存放源码
mkdir -p /cache/source
# 拷贝当前目录代码到 /cache/ 下
rsync -r ./ /cache/source --exclude=node_modules --exclude=.git
# 切换 workspace
cd /cache/source
# 执行 install
npm i
# 执行 build
npm run build
# 将构建结果拷贝到抽包地址
cp -r ./dist/* ${CUR_WORKSPACE}/.build

####### 差异: 删除时排除 node_modules 目录 #########

# 删除不需要被缓存的文件
ls -A | grep -vE "^\.$|^\.\.$|^node_modules"|xargs rm -rf

减少源码

避免在 coding 中提交 node_modules 以及各种大的二进制文件

优化编译过程

优化依赖包安装的过程

  1. 有些项目依赖了 image-minimizer-webpack-plugin, 这是一个用于压缩图片的工具,该资源依赖的 cwebp-bin 等资源需要从海外的网站下载,这个过程可能会很慢甚至失败。如果可能,建议直接提交压缩后的图片到代码库,同时去掉对这个插件的引用.
  2. 可以在编译命令前加上 time, 比如 time pnpm install 来观察这一步骤的耗时,如果这一步骤很长,可以看是否有可以去掉的依赖包,或者禁用对可选依赖包的安装,有时候升级构建工具也能使包依赖得到优化.

优化构建过程

  1. 对于 webpack 构建的应用,对 rules、plugin (如果支持) 检查是否正确设置了 exclude, 用以减少不必要的文件构建
  2. 启用构建缓存 (但缓存的持续增长还是需要关注,缓存过大的问题后续可能从产品层面得以优化)
  3. ts-loader 通常可以开启 transpileOnly: true, 并通过 fork-ts-checker-webpack-plugin 进行类型检查
  4. eslint 的优化,可以对规则进行优化,有些校验规则是非常耗时的,但同时受益并不是很大,可以考虑关闭。具体可以这么做:

4.1 设置 __TIMING__环境变量,可以启用对每个 eslint rule 的性能分析,export TIMING = 1;
4.2 在本地正常执行构建,检测 eslint rule performance 的输出,分析耗时较长的规则,确认是否必要

补充:
  • 关于 eslint 的多线程问题:对 eslint 开启多线程之后会导致 build 过程发现的规则异常不能抛出,导致规则实际会失效。该问题参考 Issue, 这个问题挺久了,一直没有得到有效解决.
  • 同时也可以考虑将 eslint 的校验作为 git hook 执行,避免提交不规范的代码,此时在 build 过程可以省略这一步骤.
5. 代码 minify 的过程,推荐使用 esbuild, 在 webpack 里面就可以配置.
{
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
minify: TerserPlugin.esbuildMinify,
}),
],
}
}

6. 对于不经常变动的部分,建议提前编译,或通过 DllPlugin 进行优化。比如行云部署项目本身依赖 monaco editor, 但每次对它的源码进行构建很耗时,所以直接将提前编译好的代码提交了,后续直接用.
7. 注意避免一个项目被 build 多次,比如:
7.1 对于使用 vue-cli-service 的应用,v5.0.0-beta.0 开始,可能会根据浏览器列表配置生成不同的包,会导致多次构建
7.2 有一些项目需要微前端接入,可能会为独立运行时、子应用模式采用不同的入口,从而构建两次。比如 JModule 的用户,由于极早期 webpack-jmodule-plugin 的版本不能自定义入口文件,通常会构建两次,建议升级为最新的 @jmodule/plugin-webpack, 并且采用同一个入口文件构建一次.
8. 如果是一个相对简单的应用,可以考虑换其它构建工具,比如 esbuild、swc, 编程语言带来的性能差异,确实能形成降维打击.
9. 如果可能,分析项目代码间的依赖,拆分为多个构建并行执行,编译机的最大优势就是多核,咱可以充分利用.
10. 升级 webpack 以及其它构建插件,通常也能带来一定程度的速度提升,我们 jci 项目的编译就从升级中获得了一些受益.
补充:
  1. webpack 的更多细节优化,可以参考 https://webpack.docschina.org/configuration/cache/

  2. 同样这里也可以考虑在 build 命令前加 time, 比如 time npm run build, 便于观察这一步的时间.

  3. 还可以用 ‘speed-measure-webpack-plugin’ 对 webpack 的构建时长进行辅助分析.

前端构建的提速是一项比较复杂且细节的工程,目前产品上在持续跟踪构建慢的应用,努力优化编译速度,但前端本身拥有一个比较自由的技术环境,没有统一的构建工具与流程,另外语言本身的执行效率、单线程的构建也不好让编译机发挥其最大能力,所以目前全局的通用优化手段还是会比较局限,还是依赖项目自身的优化。希望大家一起努力共建美好的明天.


END



2023 GOTC 全球开源技术峰会精彩回顾



这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
独家编译:2022美国自有品牌发展报告ChatGPT官方iOS应用凌晨上线,免费无广告/微软CEO回怼马斯克/阿里云将拆分上市西单女孩励志歌“茄子”的未来:GNOME 的下一代照相应用程序“快照”应用 | Linux 中国【教育】夏天终于快来啦~ 想去海边,但只知道南法?NO,NO,NO!【还在等Babcock Tower??NONONO!】【Allston】【采光佳/空间大/有电梯】【两室9月份$3600】愚人节赢了个大奖NEJM丨首次应用CRISPR技术改造CAR T细胞治疗复发性癌症获得成功,且不会伤害健康细胞女儿最近有点emo一文解读|Java编译期注解处理器AbstractProcessor程序员神器VS Code再提速,将内置JS减小20%!橡链云加装“应用集成”引擎,智能化转型再提速新高一,如何快速建立自己的申请目标?数字员工来了!有何神通?保险业AI应用提速,行业大咖这样说这款编译器能让Python和C++一样快:最高提速百倍,MIT出品旧金山房屋费改革以加速建造住房理想智驾提速:感知换帅,芯片提速,高层赴美招人|36氪独家英语妙句“No body is nobody”,怎么理解?@23/24届澳留学生,回国求职比的就是:谁能快速建立职业"护城河"!特斯拉加速建设全球供应链;中国企业存了 50 万亿定期修改几行代码就让LLM应用提速100多倍!这个团队两周搭建ChatGPT缓存层,曾被老黄OpenAI点赞ChatGPT官方iOS应用上线,支持中文语音/周星驰新作定档/iOS 16.5推出,彩虹壁纸上线【5G+工业互联网分会场预告】深化行业应用,提速规模发展!Spring 中 @NotEmpty、@NotBlank、@NotNull,傻傻分不清楚!今日奇事夏天终于快来啦~ 想去海边,但只知道南法?NO,NO,NO!破局之作:首部开源 AIGC 软件工程应用电子书《构筑大语言模型应用:应用开发与架构设计》Chrome启用全新编译器2022南美南极行(30)南极之旅啥信号?又一批绩优基金“开门迎客”,这些新基火速建仓!麻省理工Python增强编译器Codon 让Python像C\\C++一样高效ChatGPT官方iOS应用正式上线,内购Plus服务助力畅快聊天!这款编译器能让Python和C++一样快!最高提速百倍,MIT出品!修改几行代码就让 LLM 应用提速 100 多倍!这个团队两周搭建 ChatGPT 缓存层,曾被老黄 OpenAI 点赞Node.js 20 正式发布
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。