您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Nodejs 应用编译构建提速建议
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Nodejs 应用编译构建提速建议
fe****
2023-05-24
IP归属:北京
8720浏览
## 编译构建的整体过程 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/ 目录大小: 在编译命令中加入: ```du -sh /cache```, 通过构建日志查看目录大小 2. 在整体编译命令前后都加上 ```date```, 可以看自己项目的构建过程耗时, 即编译命令执行时间 3. 在主要的编译命令的每一行前面加上 ```time ```, eg: ```time npm install``` 可以看 install 过程的实际耗时, build 过程同理. 4. 对比整体构建时间(网页上直接显示的任务时间)与编译命令执行时间(末尾的 date 时间 - 开头的 date 时间), 如果整体时间超过编译命令执行时间很多(> 1min30s), 可能是 /cache/ 目录或镜像过大导致的, 可以咚咚联系 [行云部署(j-one)](timline://chat/?topin=j-one) 以下为详情介绍: ## 使用更小的运行镜像 如果有较大的镜像, 建议联系运维进行优化. 详情可以查看这篇文章: [原来这才是编译构建慢的原因!(持续更新)](http://sd.jd.com/article/11375?shareId=57137&isHideShareButton=1) ## 善用持久缓存 缓存可以对应用构建带来提速的效果, 但如果缓存目录持续增长, 大到一定程度反倒可能让速度变慢. ### 了解缓存机制: 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)](timline://chat/?topin=j-one) 对应用缓存进行清理 ### 解决缓存跨盘造成的性能损失 __主要思路__: 使源码与 /cache/ 处于同一个文件系统. 目前对于 pnpm 的应用推荐该方式. __原理__: 使源码与 /cache/ 处于同一个文件系统, 这可以让 pnpm 的 hard link 方式生效, 相对于node_modules那些数以万计的小文件复制, 执行效率会得到可观的提升. 参考: [Pnpm 是否可以跨多个驱动器或文件系统工作?](https://pnpm.io/zh/faq) __方式__: 将当前工作目录的代码复制到 /cache/ 下再执行 install、build 命令. __参考命令__: ```bash # 记下当前工作目录 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](https://webpack.docschina.org/configuration/cache/) 如果是 monorepo 的应用, 还可以实现子项目级别的缓存, 比如使用 [nx](https://nx.dev/) 进行monorepo 的管理, 则可以配置 NX_CACHE_DIRECTORY 来设置缓存地址, eg: ```bash export NX_CACHE_DIRECTORY=/cache/jdos3-console-ui/.nx ``` eslint 也是一个很费时的操作, 它也支持缓存, 但默认不开启, 如果有需要也可以开启缓存, 但缓存策略需要使用 'content', 因为每次构建文件的 createTime 都会改变, metadata 的策略会失灵. 参考: [eslint cache](https://eslint.org/docs/latest/use/command-line-interface#--cache) 通常我们需要同时兼容本地开发和行云部署的构建, 可以通过环境变量的方式实现, 以 webpack5 为例: webpack5 的缓存配置: ```javascript { cache: { type: 'filesystem', profile: true, cacheDirectory: process.env.BUILD_CACHE_DIRECTORY, compression: 'gzip', }, } ``` 同时在行云部署的编译命令中增加: ```bash export BUILD_CACHE_DIRECTORY=/cache/.webpack ``` ### 另一种利用缓存的思路: 缓存 node_modules (编译团队提出了这种思路, 我目前没有进行相关尝试, 产品上针对该思路的通用解决方案在探索中) __主要思路__: 模拟本地构建(本地构建会持久保留 node_modules目录) __收益__: 1. 加速 install 的过程, 减少包的安装. 2. 利用代码构建缓存: webpack5 或 babel-loader 等一般会在 node_modules/.cache目录下存放构建缓存, 这也是很多应用本地构建较快的原因. 当然 .cache 目录会持续增长, 需要定时清理, 有兴趣大家可以看看本地的代码里是否有这个目录, 占多大空间. __参考命令__: 大体上与上面 '解决缓存跨盘造成的性能损失' 过程相同, 只是最后rm 的过程保留 node_modules 目录, 以供下次使用 ```bash ####### 与上面 解决缓存跨盘造成的性能损失 一致 ######### # 记下当前工作目录 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](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#typescript-options) 进行类型检查 4. eslint的优化, 可以对规则进行优化, 有些校验规则是非常耗时的, 但同时受益并不是很大, 可以考虑关闭. 具体可以这么做: 4.1 设置 __TIMING__环境变量, 可以启用对每个 eslint rule 的性能分析, ```export TIMING = 1```; 4.2 在本地正常执行构建, 检测 eslint rule performance 的输出, 分析耗时较长的规则, 确认是否必要 __补充__: 1. 关于eslint的 __多线程__ 问题: 对eslint开启多线程之后会导致 build 过程发现的规则异常不能抛出, 导致规则实际会失效. 该问题参考 [Issue](https://github.com/webpack-contrib/eslint-webpack-plugin/issues/146), 这个问题挺久了, 一直没有得到有效解决. 2. 同时也可以考虑将 eslint 的校验作为 git hook 执行, 避免提交不规范的代码, 此时在 build 过程可以省略这一步骤. 5. 代码 minify 的过程, 推荐使用 esbuild, 在webpack里面就可以配置. ```javascript { optimization: { minimize: true, minimizer: [ new TerserPlugin({ minify: TerserPlugin.esbuildMinify, }), ], } } ``` 6. 对于不经常变动的部分, 建议提前编译, 或通过 [DllPlugin](https://webpack.js.org/plugins/dll-plugin/) 进行优化. 比如行云部署项目本身依赖 monaco editor, 但每次对它的源码进行构建很耗时, 所以直接将提前编译好的代码提交了, 后续直接用. 7. 注意避免一个项目被 build 多次, 比如: 7.1 对于使用 vue-cli-service 的应用, v5.0.0-beta.0 开始, 可能会根据浏览器列表配置生成不同的包, 会导致多次构建 7.2 有一些项目需要微前端接入, 可能会为独立运行时、子应用模式采用不同的入口, 从而构建两次. 比如 [JModule](https://jmodule.jd.com) 的用户, 由于极早期 webpack-jmodule-plugin 的版本不能自定义入口文件, 通常会构建两次, 建议升级为最新的 @jmodule/plugin-webpack, 并且采用同一个入口文件构建一次. 8. 如果是一个相对简单的应用, 可以考虑换其它构建工具, 比如 esbuild、swc, 编程语言带来的性能差异, 确实能形成降维打击. 9. 如果可能, 分析项目代码间的依赖, 拆分为多个构建并行执行, 编译机的最大优势就是多核, 咱可以充分利用. 10. 升级webpack以及其它构建插件, 通常也能带来一定程度的速度提升, 我们 jci 项目的编译就从升级中获得了一些受益. __补充__: 1. webpack 的更多细节优化, 可以参考 [https://webpack.docschina.org/configuration/cache/](https://webpack.js.org/guides/build-performance/) 2. 同样这里也可以考虑在 build 命令前加 time, 比如 ```time npm run build```, 便于观察这一步的时间. 3. 还可以用 ‘speed-measure-webpack-plugin’ 对 webpack 的构建时长进行辅助分析. __前端构建的提速是一项比较复杂且细节的工程, 目前产品上在持续跟踪构建慢的应用, 努力优化编译速度, 但前端本身拥有一个比较自由的技术环境, 没有统一的构建工具与流程, 另外语言本身的执行效率、单线程的构建也不好让编译机发挥其最大能力, 所以目前全局的通用优化手段还是会比较局限, 还是依赖项目自身的优化. 希望大家一起努力共建美好的明天.__
上一篇:Velocity不用愁!Velocity系统的前端工程化之路
下一篇:Elasticsearch之join关联查询及使用场景
fe****
文章数
2
阅读量
269
作者其他文章
01
Nodejs 应用编译构建提速建议
编译构建的整体过程拉取编译镜像拉取缓存镜像拉取项目源码挂载缓存目录执行编译命令(用户自定义)持久化缓存上传编译镜像为什么在本地构建就快, 但编译机上很慢在编辑机上每次的构建环境都是全新的, 完成一次构建比本地需要多一些步骤:1. 现成的全局包缓存 VS 重新构建缓存: 咱可以先简单理解为咱使用 npm 的时候那个全局的缓存目录, 编辑机需要准备持久化的缓存的环境, 包括下载、挂载以重建缓存, 如果
01
行云部署前端架构解析-前言
一个简单的自我介绍项目规模截止目前上万次代码提交,总代码行数1 超过21万行,其中人工维护的代码超过 13万行,近千个文件。前端线上服务直接对接的后端服务,达十多个。跟很多应用一样, 它有行云的入口, 也有独立的服务, 还有单独的插件接口它是行云的子应用, 也是其它应用的主应用技术栈代码本身是 monorepo 的结构,通过 nx + pnpm 进行管理nx 是一个优秀的项目管理工具,可以自动分析
fe****
文章数
2
阅读量
269
作者其他文章
01
行云部署前端架构解析-前言
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号