storybook

如何減少 storybook 在 github actions 上的時間 70%

最近在重寫公司的專案,在整合 storybook 到 CI 時好好研究了一番,發現有些方法可以大幅降低在 CI 上跑的時間,於是就稍微整理了一下。

本篇文章的 repo 在這裡,可以直接 clone 下來看 commit 會比較清楚!

1. 專案建置

可以直接切到 project-initialization 的 branch,會看到專案初始化後的環境(vite + react + storybook)。

首先建立一個簡單的專案,讓我們等等可以跑這個專案的 storybook。我這邊直接用 pnpm + vite + react + github actions 來建立這個專案。

1.1 vite

create-vite 提供了許多 template 可以快速建立一個新的專案,因此只要輸入以下指令:

$ pnpm create vite storybook-github-actions-optimization --template react-swc-ts

就可以快速建立一個 react + TypeScript + swc 的專案。

1.2 storybook

直接根據 storybook 的官網給的指令:

$ pnpm create storybook@latest
╭──────────────────────────────────────────────────────╮
│                                                      │
│   Adding Storybook version 8.6.0 to your project..   │
│                                                      │
╰──────────────────────────────────────────────────────╯
? What do you want to use Storybook for? ›  
Instructions:
    ↑/↓: Highlight option
    ←/→/[space]: Toggle selection
    a: Toggle all
    enter/return: Complete answer
◯   Documentation: MDX, auto-generated component docs
◯   Testing: Fast browser-based component tests, watch mode

這時候會出現一些提示:

直接按 enter,storybook 就會幫你初始化環境了。

1.3 Install storybook test-runner

接著要安裝 storybook test-runner,test-runner 可以讓你確認是否有 story 是整個壞掉,無法呈現 UI 的狀態,因此很適合整合到 CI 上,確保不會意外改壞 storybook。

$ pnpm add --save-dev @storybook/test-runner

這麼一來,初步的環境就建立好了,跟開頭說的一樣,可以參考 project-initialization 這個 branch。

2. 建立 CI 的 workflow

接著我們用 github actions 來建立 test storybook 的 CI workflow。

name: Continuous Integration
on:
  push:

permissions:
  contents: read

jobs:
  run-storybook-test-runner:
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4
    - name: Install pnpm
      uses: pnpm/action-setup@v4
    - name: Use Node.js
      uses: actions/setup-node@v4
      with:
        node-version-file: '.nvmrc'
        cache: 'pnpm'
    - name: Cache node modules
      id: cache-npm
      uses: actions/cache@v4
      with:
        path: node_modules
        key: ${{ runner.os }}-build-${{ hashFiles('**/pnpm-lock.yaml') }}
        restore-keys: |
          ${{ runner.os }}-build-
          ${{ runner.os }}-
    - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }}
      name: Install dependencies
      run: pnpm install --frozen-lockfile
    - name: Install Playwright
      run: pnpx playwright install --with-deps
    - name: Build Storybook
      run: pnpm build-storybook --quiet
    - name: Serve Storybook and run tests
      run: |
        pnpx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \\
        "pnpx http-server storybook-static --port 6006 --silent" \\
        "pnpx wait-on tcp:127.0.0.1:6006 && pnpm test-storybook"

一開始的 CI 是參考 storybook 官方文件上與 github actions 整合的範例,只有差在官網的範例 package manager 是 yarn,而我這裡用的是 pnpm 而已。

3. 最佳化

前置作業都完成後,我們就可以開始來最佳化流程了!

3.1 playwright install chromium only

可以看到 CI 中,在安裝完 dependencies 後,會執行這個指令:

$ pnpx playwright install --with-deps
如果不太知道 pnpx 做了什麼是,可以參考這篇:你可能不懂npx
原理和 npx 是類似的,差在 package manager 不同而已

這個指令代表的是用 playwright 這個 package 來安裝 rendering engine,會需要這個的原因是上述提到的 storybook test-runner 會用到 rendering engine (ex: Chromium、WebKit、Firefox) 來執行,而 rendering engine 通常是安裝在系統上的,因此沒辦法透過 pnpm install 來安裝在 node_modules 裡。

但如果看仔細一點 storybook 的文件會看到,test-runner 用到的是 jest-playwright 來執行,而在 jest-playwright 的 README 有提到:browsers 這個 config 可以指定要用哪些 rendering engine 來執行測試,預設其實只會有 chromium

browsers <[(string | object)[]]>. Define browsers to run tests in.
* chromium Each test runs Chromium (default).
* firefox Each test runs Firefox.
* webkit Each test runs Webkit.

但如果照 storybook 官網提供的範例執行:pnpx playwright install --with-deps 其實會直接安裝所有的 rendering engine,可以看到跑 CI 的時候,光是透過 playwright 安裝這些東西就花了 64s

因此其實可以只安裝 chromium 就好,將剛剛的指令改成:

$ pnpx playwright install chromium --with-deps

playwright 就會只安裝 chromium 這個 rendering engine 了。

可以看到這個 commit 跑的 CI,Install Playwright 的部分從 64s 下降為 22s。

3.2 cache playwright

剛有提到 playwright 是用來執行 test-runner 的必要工具,但如果沒有特別 cache 的話,就必須每次跑 CI 的時候都要重新安裝,因此我們可以將已經安裝過的 playwright 版本給 cache 住:

# 先透過 pnpx 取得 playwright 最新的版本輸出到 $GITHUB_OUTPUT
- name: Get installed Playwright version
  id: playwright-version
  run: echo "version=$(pnpx playwright --version | sed 's/Version //')" >> $GITHUB_OUTPUT
# 用上一步輸出到 $GITHUB_OUTPUT 的版本作為 cache key
- name: Cache playwright
  id: cache-playwright
  uses: actions/cache@v4
  with:
    path: '~/.cache/ms-playwright'
    key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}
    restore-keys: |
      ${{ runner.os }}-playwright-
# 如果 cache-hit 的話,代表這個版本的 playwright 已經安裝過了,不需要重新安裝
- name: Install Playwright
  if: steps.cache-playwright.outputs.cache-hit != 'true'
  run: pnpx playwright install chromium --with-deps

這麼一來,除非 playwright 的版本有更新,否則就會直接拿已經安裝過的 cache,可以大幅降低跑 CI 的時間。

可以看到這個 commit push 上去後,再重新跑一次 CI,這次就不會重新安裝 playwright 了!

3.3 cache build storybook

storybook 的文件有提到,要用 test-runner 跑 storybook 的話,需要先 build storybook 變成靜態檔案,才能用 test-runner 跑。但我們在 push commit 的時候可能根本沒有改到 storybook 相關的檔案,卻要重新 build storybook 好像不太合理,因此在這個步驟我們也可以做 cache。

- name: Cache Storybook
  id: cache-storybook
  uses: actions/cache@v4
  with:
    path: storybook-static
    # src/stories 底下的檔案有變動的話,cache key 才會變,才會需要重 build storybook
    key: ${{ runner.os }}-storybook-${{ hashFiles('src/stories') }}
    restore-keys: |
      ${{ runner.os }}-storybook-
- name: Build Storybook
  if: steps.cache-storybook.outputs.cache-hit != 'true'
  run: pnpm build-storybook --quiet

這麼一來,push commit 時,如果該 commit 沒有改到 src/stories 裡面的檔案,在跑 CI 時就不會重新 build storybook 了!

對應的 commit (不過這個 repo 的 stories 很少,因此成效會不太明顯,但隨著專案越來越大,減少的時間也會越來越可觀)。

4. 成效 ↓ 70%

可以觀察到,最一開始完全按照官網的範例的話,整個 CI 執行了 98s

而在我們做完最佳化後,整個 CI 只跑了 30s 整整下降了將近 70%

5. Reference

Test runner | Storybook docs
Running tests using Jest & Playwright

如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top