记一次CI构建优化

目前的前端项目是在gitlab CI中进行构建和部署。最近发现整个CI的构建时间比较长,比较影响开发体验,因此需要优化一下。

<!--more-->

本文首先介绍gitla CI的一些与构建流程相关的配置项,然后尝试从这些配置项优化构建速度

1. gitlab CI/CD

在传统流程中,开发需要将更新后的代码手动部署到测试or生产服务器上,整个部署流程涉及到安装依赖、编译、打包等流程,比较繁琐,会占用额外的开发时间。

因此,我们需要使用CI/CD自动化处理这些问题。

gitlab本身提供了CI功能,可以在git项目根目录下配置.gitlab-ci.yml文件,用来定义了这个项目该如何构建。

首先了解下gitlab ci的配置项,最直接的还是看看官方文档

也可以参考下面的教程

2. 问题定位

在优化之前的CI配置比较简单

image: node:12.14.0

stages:
  - build

cache:
  paths:
    - node_modules/

before_script:
  - git remote set-url origin xxx
  - npm install --registry=https://registry.npm.taobao.org

after_script:
  - node scripts/publish.js --branch ${CI_COMMIT_REF_NAME}

build_test:
  stage: build
  only:
    - test
  script:
    - npm run build:test

build_prod:
  stage: build
  only:
    - master
  script:
    - npm run build

主要就是installbuild两个步骤,且没有拆分成独立的job。为了减少npm i的耗时,对node_modules目录进行了缓存处理,整个项目构建需要大概五六分钟(300s)

该项目使用vue-cli构建,也使用了一些优化构建速度的措施,整个项目本身构建耗时

  • 在本地16G、i7的mbp上面大概是50s左右,
  • gitlab Runner分配的配置也还可以(4核8G),构建用时大概是90s左右

由于有node_modules缓存的存在,在不添加新依赖的情况下,安装速度也比较快,10s左右。

那么剩下多出来的300 - 90 - 10 耗时到底是花在哪里去了呢?

在观察job的打包日志之后发现了一个问题:在after_script脚本运行之后,按理说整个构建就已经结束了,但整个job还要运行很长一段时间才会结束

node_modules/: found 396162 matching files

这里是在创建node_modules的缓存,怎么有这么多文件需要缓存?

需要回头来看一看cache关键字

3. 缓存配置

参考

3.1. cache配置项

看一下cache关键字的配置文档

cache用来指定需要在job之间缓存的文件或目录,可以配置全局cache,也可以在某个job下面配置cache,后者会覆盖前者。

cache:paths

配置需要被缓存的文件或目录

cache:key

缓存是在jobs之前进行共享的。如果想在不同的job之间使用不同的cache,则需要配置不同的cache:key

cache:policy

默认缓存是拉取且推送的,可以配置某个job按照指定策略使用缓存

  • pull,拉取缓存
  • push,推送缓存
  • pull-push,拉取且推送缓存(默认)

3.2. 优化

为什么要做缓存呢? 主要是为了重复运行任务的时候不会重复安装全部node_modules的包,从而减少整体构建时间。

那为什么每次都要缓存?

这是必须的,项目会添加或移除某些依赖,为了保证后续构建能正常进行,因此需要更新缓存,也就是说每次都要npm i然后把新的node_modules缓存起来。

整体流程简化为:拉取缓存—>下载依赖->打包->上传->创建缓存。

回到上面的问题,看起来耗时就出现在了创建缓存这一步。

找到了问题所在,就可以考虑优化思路了:既然创建缓存这一步比较耗时,那么就尽可能不走这一步。

我们先把job拆分成安装和打包两个步骤,这样就可以使用job级别的cache来控制缓存

image: node:12.14.0

stages:
  - pre-install
  - build

pre_install:
  stage: pre-install
  script:
    - npm install --registry=https://registry.npm.taobao.org
  cache:
    key: "node_modules"
    paths:
      - node_modules/
    policy: pull-push

build_test:
  stage: build
  only:
    - test
  after_script:
    - node scripts/publish.js --branch ${CI_COMMIT_REF_NAME}
  cache:
    key: "node_modules" # 使用与pre_install相同的cache:key
    paths:
      - node_modules/
    policy: pull

  script:
    - npm run build:test

pre_installjob中,配置pull-push策略缓存,同时执行安装依赖的工作

build_testjob中,只需要pull缓存,然后执行打包即可。

不同的job职责分清之后,就可以进行优化了。看起来拉取缓存、下载依赖和创建缓存在package.json没有变化时是可以跳过的,只要不执行pre_install,就不会进行缓存的拉去和推送,也就避开了创建缓存比较耗时的问题。

恰好job的配置项only:changes可以指定某些文件变化时再触发

pre_install:
  stage: pre-install
  only:
    # 这里貌似不能使用refs 指定分支,否则会忽略changes
    changes:
      - package.json
      - package-lock.json
  except:
    - /develop|feature.*?/
  script:
    - npm install --unsafe-perm
  cache:
    key: "web_node_modules"
    paths:
      - node_modules/
    policy: pull-push

# 定义各种build job

这样,只有在依赖更新的时候,才会执行pre_installjob,常规的业务迭代只会执行buildjob

  • 第一次pre_install + build,耗时250s
  • 第二次只有build,耗时160s

相比之前每次都需要耗费300s+,非常明显地缩短了打包时间。

这里需要注意的是如果同时配置了only:changesonly:refs,就会忽略only:changes

目前推荐使用rules来控制job的触发时机,但由于公司gitlab使用的runner版本较老,还不支持该规则,因此还是使用only:changes来实现。

此外,随着打包任务的增加,node_modules/.cache目录里面的文件也会越来越多(如vue-loader、babel-loader等文件缓存),这会导致在push cache的时候耗时增加,可以定期清理重置cache,减少缓存的文件数量。

4. 小结

本文总结了如何通过拆分job并配置不同的缓存策略来避免前端项目构建耗时较长的问题。目前在线上运行了一段时间,感觉优化效果还是比较明显的。

对于开发工作之外的问题,一劳永逸的自动化确实是个不错的选择。