0

Nodejs 应用编译构建提速建议 - 京东云技术团队

 1 year ago
source link: https://www.cnblogs.com/jingdongkeji/p/17433947.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

编译构建的整体过程

  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 的构建时长进行辅助分析.

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

作者:京东科技 林光辉

内容来源:京东云开发者社区


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK