如何用 jpgboost-cli 在 CI 流水線中自動壓縮圖片

一個不斷堆積未壓縮 JPEG 與 PNG 的儲存庫,既會拖累 LCP,也會讓每一次複製變得更沉重。jpgboost-cli 可以把壓縮這一步直接自動化到 CI 流水線裡,不必仰賴每位貢獻者的自覺。本文介紹它與 GitLab CI 的整合,比較兩種截然相反的策略,並整理出那些在正式環境中可能代價高昂的陷阱。

未壓縮圖片的真實代價

每一張未經壓縮就提交的圖片,都會以包含其內容的 blob 形式在 Git 歷史中留下痕跡。即使這張圖片後來被更輕的版本取代,舊的 blob 依然留在歷史裡。git clone 也會把這段歷史一併取回。因此舊圖片的體積會持續壓在儲存庫上,即使它們早已不再使用。

在一個數個月來不斷累積螢幕截圖、行銷素材或設計匯出檔的儲存庫中,這些無用的重量很快就會達到數百 MB。這會拖慢 CI 中的複製速度,並增加所有貢獻者的頻寬耗用。

在正式環境這一端,問題並不止於儲存庫。尺寸過大的圖片會直接損害 Largest Contentful Paint(LCP),尤其當首屏中最大的那張圖片正好是決定該指標的元素時。壓縮帶來的成效可能相當可觀:在 jpgboost-cli 官方文件自己的範例中,一張 4.2 MB 的 JPEG 降到僅 890 KB,減少了 79 %。

手動壓縮無法規模化,它仰賴個人習慣。每一次合併請求,每位貢獻者都得記得在提交前最佳化圖片。實際情況是,一旦時間吃緊或事情變得急迫,這一步很容易被略過。問題通常要到數週之後才被發現,等到某次稽核或效能報告揭露出累積的債務。

為什麼要在 CI 裡自動化,而不是在本機或伺服器端

在本機壓縮圖片,例如透過 Git 的 pre-commit 掛鉤,始終取決於機器與貢獻者的自律。掛鉤可以被繞過、被移除,或者在某位直接提交檔案的設計師機器上根本就沒安裝。

而伺服器端的壓縮則介入得太晚。圖片已經被索引,甚至可能至少已經被提供過一次。也就是說,問題對最早的訪客是看得見的,而且此後每次部署都得重做一次最佳化。

CI 是最可靠的檢查點。它在每一次合併請求時執行,不受本機環境設定影響,並在作業日誌中留下看得見、可追溯的結果。正是這個唯一的必經關卡讓自動化變得值得,儘管它也帶來了從一開始就必須計入的基礎架構成本。

這項成本在此處更為沉重,因為 jpgboost-cli 並不是一個可以用套件管理器安裝的獨立執行檔。它隨 JPGBoost.app 一併發布,僅在 macOS 15 以上版本執行,並且需要 Pro 授權。

因此本文接下來的內容建立在一個具體前提上:一台持續存在的自架 macOS Runner,以取得穩定的環境,並讓 JPGBoost.app 的安裝狀態在各個作業之間保留下來。

安裝 jpgboost-cli 並在本機測試

無論是在 Runner 上還是本機測試,執行檔都位於應用程式套件內部。只要建立一次符號連結,之後就能從任何資料夾呼叫它。

# 建立一次符號連結,之後即可從任何資料夾呼叫 jpgboost-cli
sudo ln -s /Applications/JPGBoost.app/Contents/MacOS/jpgboost-cli /usr/local/bin/jpgboost-cli

# 確認指令有回應
jpgboost-cli --help

接著在 Runner 上手動啟用一次 Pro 授權。這個步驟絕不會發生在流水線內部。如果這是啟用該授權的第一次安裝,請使用購買後透過電子郵件收到的權杖。

# 本機識別碼,必要時可提供給技術支援
jpgboost-cli --machine-id

# 首次啟用,使用購買後透過電子郵件收到的權杖
jpgboost-cli --activate ACT-XXXX-XXXX-XXXX-XXXX-XXXX

如果該授權已經在別處啟用過(通常是在應用程式中,供個人使用),--activate 就不再是正確的做法了。Runner 需要加入這個既有的授權,而不是啟用一個新的。請在已啟用的裝置上產生一組配對碼,然後在 Runner 上使用它。

# 在已啟用的裝置上(應用程式或另一個 CLI)產生配對碼
jpgboost-cli --add-device

# 在 Runner 上用這組配對碼加入既有授權
jpgboost-cli --pair XXXX-XXXX

無論採用哪一種方式,這次啟用都會佔用 Pro 授權所涵蓋的兩次安裝名額之一。由於應用程式與 CLI 算作兩次獨立安裝,如果每個作業都啟用一次,名額很快就會用盡:僅僅兩次執行,授權就已經耗光了。這正是 Runner 必須持續存在的原因,好讓啟用狀態在各個作業之間保留下來。

繼續之前,先在本機快速測試一下。

# 將兩個檔案以品質 60 轉換為 WebP
jpgboost-cli photo1.jpg photo2.png --quality 60 --format webp --output ./compressed

設定 GitLab(Runner 與推送權杖)

在貼上下面的 .gitlab-ci.yml 之前,需要在 GitLab 這一端一次性準備好兩件事:註冊 Runner,以及建立修正作業推送時所需的權杖。

註冊 Runner

在選定作為常駐 Runner 的那台 Mac 上(也就是上一節中已安裝好 jpgboost-cli 及其授權的那台),使用兩個作業共用的 macos 標籤註冊 Runner。

gitlab-runner register \
  --url https://gitlab.com \
  --token <PROJECT_REGISTRATION_TOKEN>

註冊權杖位於 Settings > CI/CD > Runners > New project runner(介面會為這次註冊產生一組一次性權杖,而不是舊有的共用註冊權杖)。這裡的 --executor shell 相當關鍵:與 Docker executor 不同,它直接在 Runner 的系統上執行指令,而這正是能在各個作業之間沿用已安裝的 jpgboost-cli 與已啟用的授權、而非每次都從乾淨環境重新開始的關鍵。

GitLab 的 Create project runner 頁面,Tags 欄位填入 macos,底部是 Create runner 按鈕
這裡填入的標籤,正是下文作業所引用的那個標籤
GitLab 的 Register runner 頁面,旁邊是正在執行 gitlab-runner register 的終端機,以互動方式詢問 Runner 名稱與 executor,已輸入 shell
gitlab-runner register 會以互動方式先詢問 Runner 名稱,再詢問 executor;最後這個問題請回答 shell
GitLab 的 CI/CD 設定頁面,顯示一個已指派的專案 Runner,處於上線且閒置狀態,帶有 macos 標籤
註冊完成後,Runner 會在專案的 CI/CD 設定中顯示為上線且閒置

建立推送權杖

fix-image-weight 作業會將一個提交推送到合併請求的分支。每個作業自動取得的 CI_JOB_TOKEN 對受保護分支並不具備所需的寫入權限,因此需要一組專用權杖。

Project Access Token(專案的 Settings > Access Tokens)理論上是最乾淨的選擇:它繫結於專案而非某個帳號,即使建立者離開也依然有效。但在免費方案的個人命名空間下,GitLab.com 並未提供這項功能,它僅開放給付費群組。這也是最常見、轉而改用 Personal Access Token 的原因。

Personal Access Token 是在使用者帳號設定中建立的,而不是專案設定(Avatar > Edit profile > Access Tokens),設定如下:

  • 範圍 write_repository
  • 與你的權杖輪替政策一致的有效期限
GitLab 的 Personal access tokens 頁面,正在建立名為 push-images-ci 的權杖,範圍為 Write repository,有效期限一年
這組權杖只需要 Write repository 範圍,其他都不必

選擇這條路之前必須知道的代價:權杖繫結於建立它的帳號。如果該帳號被停用、失去專案存取權,或是那個人離開了團隊,修正作業就再也推送不上去,而且事前不會有任何警告。在團隊專案中,最好改用專屬的服務帳號來建立,而不是某位貢獻者的個人帳號。

產生的權杖只會顯示一次:請立刻複製,接著在專案的 Settings > CI/CD > Variables 中以 PUSH_TOKEN 為名新增。如果你的合併請求指向受保護分支,請勾選 MaskedProtected 兩個選項。

GitLab 的 CI/CD Settings 頁面,新增專案變數的面板中鍵為 PUSH_TOKEN,已選取 Masked 選項,數值被遮蔽
這個變數名為 PUSH_TOKEN,會在後文的 .gitlab-ci.yml 中使用

如果合併請求的來源分支本身也受保護,與該權杖關聯的帳號還必須具備直接推送到該分支的權限,位置在 Settings > Repository > Protected branches > Allowed to push and merge;否則即使權杖有效,作業中的 git push 仍會因存取遭拒而失敗。

.gitlab-ci.yml 檔案該放在哪裡

在預設情況下,GitLab 只會在一個位置尋找流水線:儲存庫根目錄下、與 README 同一層、名稱恰好為 .gitlab-ci.yml 的檔案(含開頭的點)。放在子資料夾裡的檔案,或名稱不同的檔案,都會直接被忽略:沒有任何錯誤提示,專案只是表現得像從未設定過 CI 一樣。

如果確實需要別的位置,例如多個儲存庫共用的流水線檔案,或是 monorepo 結構,可以在 Settings > CI/CD > General pipelines > CI/CD configuration file 中指向另一個路徑,甚至可以指向另一個專案,語法為 路徑/檔案.yml@群組/專案:分支。除了這種特殊情況之外,請將此欄位留空,並把檔案保留在根目錄。

檔案一旦提交並推送,就不需要任何手動啟用:GitLab 會在下一個符合檔案中某條規則的事件上自動偵測到它,此處就是透過 $CI_PIPELINE_SOURCE == "merge_request_event" 觸發的合併請求建立或更新。結果會出現在專案的 Pipelines 分頁,以及合併請求中同名的分頁裡。如果想在推送前就檢查語法,而不是等第一條流水線失敗才發現,內建編輯器(專案選單中的 Build > Pipeline editor)提供了 Validate 按鈕,它會呼叫 GitLab 的 CI 語法檢查器,而不會真的觸發一條流水線。

狀態為 Passed 的 GitLab 流水線頁面,check-image-weight 與 fix-image-weight 兩個作業皆已成功
流水線會在合併請求上自動觸發,包含 verificationcorrection 兩個作業

與 GitLab CI 整合

下面的作業指向帶有 macos 標籤的 Runner,也就是前文所述、並已在流水線之外完成啟用的那台常駐自架 Runner。它只會在合併請求的流水線中執行,並把下一節介紹的兩種模式結合起來:先檢查體積,再自動修正。

# .gitlab-ci.yml
stages:
  - verification
  - correction

variables:
  # 壓縮後圖片超過此門檻值即判定作業失敗(單位 KB)
  THRESHOLD_KB: "6000"
  # 完整複製:否則淺層複製可能不包含
  # CI_MERGE_REQUEST_DIFF_BASE_SHA,導致下面的 "git diff" 失敗。
  GIT_DEPTH: "0"

check-image-weight:
  stage: verification
  tags: [macos]
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - |
      # 繼續之前先確認 jpgboost-cli 有回應
      jpgboost-cli --help > /dev/null

      # 只處理本次合併請求中新增或修改的圖片
      # (目前範圍:.jpg 與 .png)
      FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
      if [ -z "$FILES" ]; then
        echo "本次合併請求沒有修改任何圖片。"
        exit 0
      fi

      # 試壓縮為 WebP 並輸出到暫存資料夾,用於比較體積
      mkdir -p /tmp/check-images
      # (不使用 "readarray":macOS 至今仍隨附 bash 3.2,
      # 該內建指令要到 bash 4 才引入,在這裡並不存在)
      FILES_ARR=()
      while IFS= read -r LINE; do
        FILES_ARR+=("$LINE")
      done <<< "$FILES"
      jpgboost-cli "${FILES_ARR[@]}" --format webp --quality 75 --jobs 4 --output /tmp/check-images --json > /tmp/report.json

      # 若壓縮後仍有圖片超過門檻值,則判定作業失敗
      THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
      OVER_LIMIT=$(jq --argjson threshold "$THRESHOLD_BYTES" '[.[] | select(.compressedSizeBytes > $threshold)] | length' /tmp/report.json)
      if [ "$OVER_LIMIT" -gt 0 ]; then
        echo "壓縮後仍超過 $THRESHOLD_KB KB 的圖片共有 $OVER_LIMIT 張:"
        jq --argjson threshold "$THRESHOLD_BYTES" -r '.[] | select(.compressedSizeBytes > $threshold) | .path' /tmp/report.json
        exit 1
      fi
      echo "所有被修改的圖片都在 $THRESHOLD_KB KB 門檻值之內。"

fix-image-weight:
  stage: correction
  tags: [macos]
  # 與 "verification" 階段脫鉤:否則在唯一真正需要它的情境下
  # (check-image-weight 失敗時),這個作業永遠都執行不到。
  needs: []
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - |
      # 只處理 .jpg/.png,參見 check-image-weight 中的同類註解。
      FILES=$(git diff --name-only --diff-filter=ACM "$CI_MERGE_REQUEST_DIFF_BASE_SHA" -- '*.jpg' '*.png')
      if [ -z "$FILES" ]; then
        echo "本次合併請求沒有修改任何圖片。"
        exit 0
      fi

      rm -f /tmp/report.json
      THRESHOLD_BYTES=$((THRESHOLD_KB * 1000))
      MIN_QUALITY=20
      REMAINING=""

      # 將每個檔案都轉換為 JPEG(--format jpeg,即預設值):對照片而言,
      # PNG 的壓縮效果明顯不如 JPEG(見「常見的陷阱」),而 JPEG 正是這裡
      # 期望的目標格式。不適用於帶透明度的 PNG(JPEG 沒有 alpha 色版),
      # 對照片來說不成問題,但若哪天有標誌或圖示要走這條流水線,
      # 就必須重新評估。
      #
      # 只要結果仍超過門檻值,就逐級降低品質:以固定品質(60)跑一次
      # 並不總是足夠。
      while IFS= read -r FILE; do
        FOLDER=$(dirname "$FILE")
        QUALITY=60
        while :; do
          RESULT=$(jpgboost-cli "$FILE" --format jpeg --quality "$QUALITY" --output "$FOLDER" --json)
          SIZE=$(echo "$RESULT" | jq '.[0].compressedSizeBytes // 0')
          if [ "$SIZE" -le "$THRESHOLD_BYTES" ] || [ "$QUALITY" -le "$MIN_QUALITY" ]; then
            break
          fi
          QUALITY=$((QUALITY - 15))
          if [ "$QUALITY" -lt "$MIN_QUALITY" ]; then
            QUALITY=$MIN_QUALITY
          fi
        done
        echo "$RESULT" >> /tmp/report.json
        # 輸出檔案一律是 <主檔名>.jpg:如果原始檔案本來就不是這個副檔名
        # (例如 .png),就必須把原始檔案從儲存庫中移除,否則兩者會並存,
        # 舊的那個將無限期地保持未壓縮狀態。
        case "$FILE" in
          *.jpg) : ;;
          *) git rm -q "$FILE" ;;
        esac
        if [ "$SIZE" -gt "$THRESHOLD_BYTES" ]; then
          echo "$FILE 壓縮後仍超過 $THRESHOLD_KB KB(品質 $QUALITY,$((SIZE / 1000)) KB):需要手動縮減。"
          REMAINING="$REMAINING $FILE"
        fi
      done <<< "$FILES"

      # 壓縮成效,可在作業日誌中查看(見「衡量成效」)。
      # -s:/tmp/report.json 中每個檔案對應一個 JSON 陣列(上面迴圈裡
      # 每次 ">>" 寫入一個),"add" 會先把它們合併成一個再加總。
      echo "--- 壓縮成效 ---"
      jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) 位元組 (\(.ratio))"' /tmp/report.json
      echo "壓縮前合計:$(jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json) 位元組"
      echo "壓縮後合計:$(jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json) 位元組"

      # git diff --quiet 察覺不到被 git rm 移除的 .png 被一個未追蹤的
      # .jpg 取代:status --porcelain 同樣涵蓋未追蹤的檔案。
      if [ -z "$(git status --porcelain)" ]; then
        echo "沒有需要提交的內容,所有圖片先前就已壓縮過。"
        exit 0
      fi

      git config user.name "jpgboost-ci"
      git config user.email "ci@example.com"
      git add -A
      # [skip ci]:少了它,這次推送會觸發新的 merge_request_event 流水線,
      # 後者又推送一個修正提交,如此反覆(無限迴圈)。
      git commit -m "以 jpgboost-cli 壓縮被修改的圖片 [skip ci]"
      # PUSH_TOKEN 是一組 personal access token(範圍 write_repository),
      # 以遮蔽的 CI/CD 變數形式儲存:預設的 CI_JOB_TOKEN 不足以推送到
      # 受保護分支。
      git remote set-url origin "https://gitlab-ci-token:${PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
      git push origin "HEAD:${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}"

      # 無論如何都會推送目前所能達到的最佳結果(總比什麼都不做好),但只要
      # 還有檔案在最低品質下仍超過門檻值,作業就判定失敗,讓問題保持可見,
      # 而不是被默默接受。
      if [ -n "$REMAINING" ]; then
        echo "壓縮後仍然過大的檔案:$REMAINING"
        exit 1
      fi

檢查模式還是修正模式

上一節的兩個作業展示了將 jpgboost-cli 接入 GitLab 合併請求的兩種相反做法。一種是攔下來,另一種是替貢獻者改好。

檢查模式下,check-image-weight 作業什麼都不會更動。它把每張被修改的圖片壓縮到一個暫存資料夾,將得到的體積與設定的門檻值比較,一旦超出就讓 CI 失敗。儲存庫維持原狀。重新處理圖片並再次提交是貢獻者的事;流水線只是在那之前拒絕合併。

修正模式下,fix-image-weight 作業更進一步。它直接在儲存庫中就地壓縮每張圖片,接著提交並把結果推送到合併請求的分支上。貢獻者不必再重做任何事,但 Git 歷史裡會多出一個並非他們自己寫下的提交。

比較項目檢查模式修正模式
目標圖片超過門檻值時攔下 CI壓縮並向合併請求推送一個提交
機制--json + jq,與設定的門檻值比較,接著明確執行 exit 1(並沒有內建的門檻值參數)直接壓縮、就地寫入,然後推送
對儲存庫的影響沒有影響,作業只負責觀察合併請求中新增一個自動提交
對貢獻者的影響必須自行修正並重新提交不必再做任何事
所需驗證不需要存放在遮蔽變數中的 project access token 或 deploy token
代價產生摩擦,手動修正的負擔留給貢獻者自動改寫 Git 歷史,與受保護分支存在衝突風險

實務上,檢查模式適合那些希望在圖片壓縮入庫之前,仍由自己掌握編輯決策(裁切、修圖、格式選擇)的團隊。修正模式則適合那些寧願完全不去操心的團隊,代價是歷史中多一個自動提交,以及需要維護一組推送權杖。

最佳化作業

兩個作業都已經透過 git diff --name-only --diff-filter=ACM 把工作限縮在真正被修改的檔案上,而不是每次流水線都重新掃描整個儲存庫。在累積了數百張圖片的儲存庫裡,執行時間的差距相當可觀。

在常駐的自架 Runner 上,快取的意義並不相同。GitLab CI 的 cache: 指示詞主要是為了在每個作業都從零開始的暫時性 Runner 上還原相依套件而存在。而這裡 JPGBoost.app 已經安裝在 Runner 上,不需要重新下載。加上一個 cache: 區塊,並不會帶來 Runner 本機儲存空間之外的任何額外好處。

--json 選項提供了建立冪等機制所需的資訊。只要把 compressedSizeBytes、或者更理想的是檔案的雜湊值,在多次執行之間保留下來,就能辨識出未變動的檔案,避免每條流水線都重新壓縮一遍。

至於平行處理,則由 --jobs 原生支援。多個檔案可以同時處理,各自獨立地解碼與釋放。因此記憶體用量主要取決於 --jobs 的數值,而不是待處理檔案的總數。在 12 個檔案的批次上,文件給出的數據是 --jobs 8 相較 --jobs 1 約有 4 倍的提升,這個量級可以作為在自己 Runner 上設定該數值的參考,同時要記得 AVIF 與 JPEG XL 的編碼對 CPU 的耗用明顯高於 JPEG 或 HEIC。

常見的陷阱

第一個陷阱與輸出格式有關。上面的指令稿刻意把所有圖片,包括 PNG,全都轉換成 JPEG。對照片而言,JPEG 通常在品質與體積的比值上優於 PNG:因此強制使用 --format jpeg,可以在單純降低品質仍不足夠的情況下,把檔案壓到門檻值以下。代價則是失去透明度,因為 JPEG 沒有 alpha 色版,而且日誌中未必會出現任何錯誤訊息。這個選擇對照片有效,卻可能無聲無息地毀掉一張透明背景的 PNG 標誌或圖示。在兩種用途混雜的儲存庫裡,與其對所有圖片強加單一格式,不如把作業的適用範圍收窄(例如限定在某個專用資料夾)。

在 GitLab CI 中,某個階段的作業在預設情況下只有在上一階段的所有作業都成功時才會執行。可是 fix-image-weight 只有在 check-image-weight 偵測到超標時才有意義,而那恰恰是在預設行為下它永遠不會被執行的情境。needs: [] 讓修正作業擺脫這項隱含的相依關係,使其能夠獨立執行,與檢查作業平行進行。

接著,修正作業會將提交推送到觸發流水線的那個分支。若不加以防範,這次推送可能又觸發一條新的合併請求流水線。如果結果仍被判定過大,作業就再修正一次、再推一個提交、再觸發一條流水線。這很容易在幾分鐘之內演變成產生數十個提交與流水線的無窮迴圈。在自動提交的訊息中加入 [skip ci],就是告訴 GitLab 不要為這個提交觸發新的流水線。

針對 CI_MERGE_REQUEST_DIFF_BASE_SHA 執行的 git diff 還有一個前提:這個基準提交在本機是可取得的。然而 GitLab 預設會以有限的深度進行複製。在一個累積了大量提交的合併請求中,或是當目標分支已明顯分歧時,要尋找的那個提交可能根本就不存在。此時 git diff 會因為一個與圖片體積毫無關係的原因而失敗。設定 GIT_DEPTH: "0" 會強制完整複製並迴避這個問題,代價是每個作業的複製時間變長。

另一個陷阱與變更偵測有關。git diff --quiet 只能偵測到 Git 已經追蹤的檔案的變更。可是一旦指令稿把圖片轉成 JPEG 並用 git rm 移除原始檔案,真正的變更其實由兩個動作組成:被追蹤的 .png 的移除,以及一個全新、未被追蹤的 .jpg 的建立。這時單純的 git diff 可能什麼都回報不出來。實際情況就是,作業可以在日誌裡顯示出完全真實的壓縮成效,卻依然得出「沒有需要提交的內容」的結論,因而從未推送結果。而同樣涵蓋未追蹤檔案的 git status --porcelain 就不會這樣被騙過去。

已經存在於 Git 歷史中的圖片,本身也是一個獨立的體積問題。即使在新的提交中把它們換成了壓縮版本,舊版本依然會以 Git blob 的形式留在歷史裡。儲存庫並不會自動回收這些檔案所佔用的空間。只有改寫歷史才能清除這些舊資料,而那是一種破壞性操作,在自動化流水線裡沒有立足之地。

修正模式的自動提交還可能與儲存庫本身的規則相牴觸。受保護分支可能禁止直接推送,或要求合併前先經過審查。同樣地,某個掛鉤或其他格式化機制也可能更動同一批檔案,與壓縮作業發生衝突。這些交互作用必須明確地測試,而不是想當然地認為它們能相安無事地共存。

最後,這條流水線只涵蓋那些經過儲存庫、更確切地說是經過相應合併請求流程的圖片。如果設計師把圖片直接放進 CMS、共用資料夾或 Git 之外的任何素材庫,就完全繞過了這項檢查。流水線提升的是儲存庫中受版本控制的圖片的品質,但它本身並不構成一套完整的素材管理政策。

衡量成效

fix-image-weight 已經在壓縮迴圈之後,把這份報告輸出到自己的日誌裡(見上文「與 GitLab CI 整合」一節),可以從合併請求的 Pipelines 分頁查看,不需要再額外加上任何東西。細節在於:--json 輸出中的 ratio 欄位會與 originalSizeBytescompressedSizeBytes 並列,直接給出每個檔案的縮減百分比。

GitLab 中 fix-image-weight 作業的日誌,顯示壓縮成效報告以及每個檔案壓縮前後的大小
修正作業的日誌會列出每個檔案的成效,以及壓縮前後的合計值
jq -r '.[] | "\(.path) : \(.originalSizeBytes) -> \(.compressedSizeBytes) 位元組 (\(.ratio))"' /tmp/report.json

至於彙總合計,確切的指令取決於作業是如何寫入 /tmp/report.json 的。fix-image-weight 在迴圈過程中為每個檔案寫入一個 JSON 陣列(>>),因此最終檔案包含的是若干個串接在一起的陣列,而不是單獨一個。這時需要 -s(slurp)才能整體讀取,但它又會把這些陣列再包進一個外層陣列:若不先用 add 把它們攤平,map 就會直接在這裡出錯(jq: error: Cannot index array with string)。上面的指令稿用的正是這種寫法。

# 整批檔案壓縮前後的合計
# (分多次呼叫寫入的檔案:fix-image-weight)
jq -s 'add | map(.originalSizeBytes) | add' /tmp/report.json
jq -s 'add | map(.compressedSizeBytes) | add' /tmp/report.json

如果你是在別處自行產生報告,例如在本機(見「安裝 jpgboost-cli」),或是在一次呼叫中壓縮所有檔案的 check-image-weight 裡(>),那麼那裡的 /tmp/report.json 本身就已經是單一的 JSON 陣列,此時 -s 既沒有必要也不正確:它會因為相反的原因產生同樣的錯誤,把一個原本就完整的陣列又包了一層。

# 同樣的計算,針對一次呼叫寫成的報告
# (本機測試,或 check-image-weight)
jq 'map(.originalSizeBytes) | add' /tmp/report.json
jq 'map(.compressedSizeBytes) | add' /tmp/report.json

jpgboost-cli 只會測量它自己產出的東西:壓縮前後的體積。效能分數(LCP、Lighthouse 分數)仍然必須以專門的工具另行取得。這項工具本身並不會原生計算這些指標,而把它們與壓縮百分比人為掛鉤,也只會是一種未經驗證的外推。