🚀

WordPressテーマのCI/CDをGitHub Actionsで構築する(Minifyを追加)

に公開

はじめに

このブログ記事では、WordPressテーマのCI/CDに、画像の最適化、JavaScriptとCSSのminify処理を追加する方法について解説します。

対象読者

  • WordPressテーマのCI/CDに興味がある方
  • GitHub Actionsを使ってWordPressテーマのCI/CDを構築したい方
  • WordPressテーマのパフォーマンスを改善したい方

前提知識

  • WordPressの基本的な知識
  • CI/CDの基本的な知識
  • GitHub Actionsの基本的な知識

CI/CDとは

CI/CD(継続的インテグレーション/継続的デリバリー)とは、ソフトウェア開発における変更をより頻繁かつ確実にリリースするためのプラクティスです。

Minifyとは

Minifyとは、JavaScriptやCSSなどのファイルのサイズを小さくする処理のことです。具体的には、コメントや空白文字を削除したり、変数名を短くしたりします。

Minifyを行うメリット

  • ファイルサイズが小さくなるため、Webサイトの表示速度が向上する。
  • ファイルサイズが小さくなるため、サーバーの負荷が軽減される。
  • ファイルサイズが小さくなるため、ユーザーのデータ通信量を削減できる。

今回は、Deployする際に画像の最適化やcssのminifyと言ったwebpackが普通は行うようなビルドを少し取り入れます。

設定

jpg, pngファイルを圧縮するカスタムアクション。

このアクションは、pngquantとjpegoptimを使って、リポジトリ内のPNGとJPG画像を最適化します。

  • pngquant: PNG画像を非可逆圧縮するツールです。
  • jpegoptim: JPG画像を非可逆圧縮するツールです。

.github/actions/optimize-image/action.yml

name: 'Optimize Images with Pngquant & Jpegoptim'
description: 'Finds and optimizes PNG and JPG images in the repository using pngquant and jpegoptim.'

inputs:
  png_quality:
    description: 'Quality range for pngquant (e.g., 65-80)'
    required: false
    default: '65-80'
  jpeg_quality:
    description: 'Quality for jpegoptim (0-100, lower is smaller file, more loss)'
    required: false
    default: '80' # jpegoptim のデフォルト品質
  strip_metadata:
    description: 'Remove all metadata (EXIF, comments, etc.) from JPEGs (true/false)'
    required: false
    default: 'true'

outputs:
  optimized_png_count:
    description: 'Number of PNG images that were optimized.'
    value: ${{ steps.optimize_png.outputs.optimized_count }}
  optimized_jpg_count:
    description: 'Number of JPG images that were optimized.'
    value: ${{ steps.optimize_jpg.outputs.optimized_count }}

runs:
  using: "composite"
  steps:
    - name: Install image optimization tools
      run: |
        sudo apt-get update
        sudo apt-get install -y pngquant jpegoptim
      shell: bash

    - name: Find and optimize PNG images
      id: optimize_png # IDを明確に
      run: |
        optimized_count=0
        
        find . -type f -name "*.png" -print0 | while IFS= read -r -d $'\0' file; do
          original_size=$(stat -c %s "$file")
          pngquant --quality=${{ inputs.png_quality }} --force --ext .png "$file"
          optimized_size=$(stat -c %s "$file")
          
          if [ "$original_size" -ne "$optimized_size" ]; then
            echo "Optimized PNG: $file (Original: $original_size bytes, Optimized: $optimized_size bytes)"
            optimized_count=$((optimized_count + 1))
          else
            echo "Skipped PNG (no change): $file"
          fi
        done
        
        echo "optimized_count=$optimized_count" >> $GITHUB_OUTPUT
      shell: bash

    - name: Find and optimize JPG images
      id: optimize_jpg # IDを明確に
      run: |
        optimized_count=0
        
        find . -type f -iregex ".*\.jpe?g$" -print0 | while IFS= read -r -d $'\0' file; do
          original_size=$(stat -c %s "$file")
          
          # jpegoptimのオプションを動的に設定
          JPEGOPTIM_OPTIONS="--max=${{ inputs.jpeg_quality }}"
          if [ "${{ inputs.strip_metadata }}" == "true" ]; then
            JPEGOPTIM_OPTIONS+=" --strip-all"
          fi
          
          jpegoptim $JPEGOPTIM_OPTIONS "$file"
          
          optimized_size=$(stat -c %s "$file")
          
          if [ "$original_size" -ne "$optimized_size" ]; then
            echo "Optimized JPG: $file (Original: $original_size bytes, Optimized: $optimized_size bytes)"
            optimized_count=$((optimized_count + 1))
          else
            echo "Skipped JPG (no change): $file"
          fi
        done
        
        echo "optimized_count=$optimized_count" >> $GITHUB_OUTPUT
      shell: bash

JavaScriptのminify

このアクションは、Terserを使って、JavaScriptファイルをminify(難読化)します。

  • Terser: JavaScriptファイルをminify(難読化)するツールです。

.github/actions/optimize-js/action.yml

name: 'Uglify JavaScript with Terser'
description: 'Finds and minifies/uglifies JavaScript files using Terser.'

inputs:
  paths:
    description: 'Glob pattern for JavaScript files to process (e.g., "**/*.js").'
    required: false
    default: '**/*.js'
  terser_options:
    description: 'JSON string of Terser minify options (e.g., {"compress":{},"mangle":true})'
    required: false
    default: '{"compress":true,"mangle":true}' # デフォルトは圧縮と変数名の難読化

outputs:
  uglified_count:
    description: 'Number of JavaScript files that were uglified/minified.'
    value: ${{ steps.uglify.outputs.uglified_count }}

runs:
  using: "composite"
  steps:
    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20' # Terserの実行に必要なNode.jsのバージョン

    - name: Install Terser
      run: npm install -g terser # terserをグローバルインストール
      shell: bash

    - name: Find and uglify JavaScript files
      id: uglify
      run: |
        uglified_count=0
        
        # globパターンを配列として扱うための準備
        # find . -name "*.js" よりも柔軟な指定が可能
        
        # shopt -s globstar はワイルドカードを拡張する(bashでのみ有効)
        shopt -s globstar || true # エラーにならないように
        
        # inputs.paths で指定されたファイルを処理
        for file in ${{ inputs.paths }}; do
          if [ -f "$file" ]; then # ファイルが存在することを確認
            original_size=$(stat -c %s "$file") # オリジナルファイルのサイズを取得
            
            # Terserで難読化(上書き)
            # input: JSON文字列をparseして渡す
            terser "$file" -o "$file" --config-file <(echo '${{ inputs.terser_options }}')
            
            optimized_size=$(stat -c %s "$file") # 難読化後のファイルのサイズを取得
            
            if [ "$original_size" -ne "$optimized_size" ]; then
              echo "Uglified: $file (Original: $original_size bytes, Optimized: $optimized_size bytes)"
              uglified_count=$((uglified_count + 1))
            else
              echo "Skipped (no change): $file"
            fi
          fi
        done
        
        echo "uglified_count=$uglified_count" >> $GITHUB_OUTPUT
      shell: bash


CSSのminify

このアクションは、clean-css-cliを使って、CSSファイルをminifyします。

  • clean-css-cli: CSSファイルをminifyするツールです。

.github/actions/optimize-css/action.yml

name: 'Minify CSS with Clean-CSS'
description: 'Finds and minifies CSS files using clean-css-cli.'

inputs:
  paths:
    description: 'Glob pattern for CSS files to process (e.g., "**/*.css").'
    required: false
    default: '**/*.css'
  clean_css_options:
    description: 'JSON string of clean-css options (e.g., {"level":2})'
    required: false
    default: '{}' # デフォルトは基本的な圧縮
  output_suffix:
    description: 'Suffix to add to the minified file name (e.g., .min for style.min.css). Leave empty to overwrite.'
    required: false
    default: '' # デフォルトは上書き

outputs:
  minified_count:
    description: 'Number of CSS files that were minified.'
    value: ${{ steps.minify.outputs.minified_count }}

runs:
  using: "composite"
  steps:
    - name: Set up Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20' # clean-css-cliの実行に必要なNode.jsのバージョン

    - name: Install clean-css-cli
      run: npm install -g clean-css-cli # clean-css-cliをグローバルインストール
      shell: bash

    - name: Find and minify CSS files
      id: minify
      run: |
        minified_count=0
        
        shopt -s globstar || true
        
        for file in ${{ inputs.paths }}; do
          if [ -f "$file" ]; then # ファイルが存在することを確認
            original_size=$(stat -c %s "$file") # オリジナルファイルのサイズを取得
            
            output_file="$file"
            if [ -n "${{ inputs.output_suffix }}" ]; then
              # 拡張子の前にサフィックスを追加 (e.g., style.css -> style.min.css)
              filename="${file%.*}"
              extension="${file##*.}"
              output_file="${filename}${inputs.output_suffix}.${extension}"
            fi

            # clean-css-cliで圧縮
            # input: JSON文字列をparseして渡す
            # 圧縮後にファイルサイズが変わるか確認
            # clean-css-cli はデフォルトで上書きするが、
            # -o で出力ファイルを明示的に指定できるため、上書きまたは別名保存が可能
            
            # clean-css-cliのオプションを構築
            CLEAN_CSS_OPTIONS=""
            if [ -n "${{ inputs.clean_css_options }}" ]; then
              # clean-css-cli はJSONファイルを直接受け取らないので、
              # オプションをコマンドライン引数に変換するか、stdin経由で渡す必要がある。
              # 最も簡単なのは、JSON文字列をそのまま渡すこと。
              # clean-css-cliは --config オプションがないので、
              # 各オプションを個別に渡すか、デフォルトを使う。
              # ここでは、デフォルトの動作に任せるか、カスタムオプションを考慮せず、
              # simpleに圧縮する。より高度なオプションが必要な場合は、スクリプト内でJSONをパースして引数に変換する必要がある。
              
              # しかし、clean-css-cliは`--config`オプションがないため、オプションは個別に渡す必要があります。
              # ここでは、`clean_css_options`をJSONとしてパースして引数に変換する例を示します。
              # ただし、これを行うには`jq`などのツールが必要になるため、ワークフローの複雑さを避けるため、デフォルトではシンプルな圧縮のみを行うか、特定のよく使うオプションのみをinputで受け取る形にするのが現実的です。
              
              # 今回は`clean-css-cli`のデフォルト挙動(強力な圧縮)に任せるか、よく使うオプション(例: `--level`)を直接inputで受け取る形にします。
              # `clean_css_options` は、ここでは直接利用せず、将来的な拡張ポイントとして残します。
              
              echo "Warning: clean_css_options input is currently ignored due to clean-css-cli limitations for direct JSON config. Using default clean-css behavior or specified output_suffix."
            fi

            cleancss -o "$output_file" "$file" # 圧縮を実行

            optimized_size=$(stat -c %s "$output_file") # 圧縮後のファイルのサイズを取得
            
            if [ "$original_size" -ne "$optimized_size" ]; then
              echo "Minified: $file to $output_file (Original: $original_size bytes, Optimized: $optimized_size bytes)"
              minified_count=$((minified_count + 1))
            else
              echo "Skipped (no change): $file"
            fi
          fi
        done
        
        echo "minified_count=$minified_count" >> $GITHUB_OUTPUT
      shell: bash

Workflowへの組み込み方

これらのアクションをワークフローに組み込むには、.github/workflows/main.yml ファイルに以下のステップを追加します。

    - name: Optimize Images
      uses: ./.github/actions/optimize-image
      with:
        jpeg_quality: 70

    - name: Optimize JavaScript
      uses: ./.github/actions/optimize-js

    - name: Optimize CSS
      uses: ./.github/actions/optimize-css

これらのステップは、deploy ステップのに追加することを推奨します。

ワークフローファイルの全体像

ワークフローファイルは、以下のようになります。

name: CI/CD

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up SSH key
        uses: webfactory/ssh-agent@v0.8.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Optimize Images
        uses: ./.github/actions/optimize-image
        with:
          jpeg_quality: 70

      - name: Optimize JavaScript
        uses: ./.github/actions/optimize-js

      - name: Optimize CSS
        uses: ./.github/actions/optimize-css

      - name: Deploy
        run: |
          # デプロイ処理
          echo "Deploying..."

結果

これらの最適化を行うことで、サイトを表示した際のアセットの読み込み時間を短縮できるようになりました。

具体的には、アセットの合計サイズを11.0MBから7.7MBに削減できました。

本来であれば、webpなどのより高度な画像フォーマットを使用したいところですが、今回はオリジナルファイルを変更せずに、裏側で最適化できる範囲に留めました。

実行時間

画像の最適化に少し時間がかかります。1分程度です。

画像の圧縮ツールのインストールにネットワーク通信が必要になるため結構時間を消費しているようです。
可能なら画像は元のデータを圧縮してしまうのがおすすめです。Git管理しているのでいつでももとに戻せるので。

今後の展望

  • webpなどのより高度な画像フォーマットの導入
  • JavaScriptとCSSの更なるminify
  • CDNの導入

Discussion