paranitips

Never stop learning! がモットーのゆるふわエンジニアブログ

assets:precompileしたアセットのサイズをbundlesizeを使ってPR上に通知する

f:id:paranishian:20181204173858p:plain

アセットは放置しておくと膨らんでいくばかりなのでチェックできる仕組みをつくります。
今回は、assets:precompileしたアセットのサイズ(とmasterとの差分)をPR上に通知するようにしました。

f:id:paranishian:20181206183903p:plain

bundlesizeのセットアップ

bundlesizeの使い方はREADMEを見ていただければと。
siddharthkp/bundlesize: Keep your bundle size in check

今回は、precompileしたアセットを対象にするので以下のようにしました 👇

// package.json
...
  "bundlesize": [
    {
      "path": "./public/assets/application.js.gz",
      "maxSize": "150 kB"
    },
    {
      "path": "./public/assets/application.css.gz",
      "maxSize": "30 kB"
    }
  ],
...

bundlesizeには、PR上で同じファイル名のもののサイズ差分を教えてくれる便利な機能があります。
が、rails4からはdigestが必ずついてしまうのでアセットが別名になってしまってサイズの比較ができない。。🤔

ので、compile後のアセットをnon-digestなファイル名にコピーすることで対応しました。

non-digestなアセットをつくるgemもありますが、testのためにconfig/initializersに手を加えるのは大げさすぎるのでやめました 😪
alexspeller/non-stupid-digest-assets: Fix the Rails 4 asset pipeline to generate non-digest along with digest assets

CircleCIに導入する

BUNDLESIZE_GITHUB_TOKENは別途CircleCIの「Environment Variables」に設定しておきます。 f:id:paranishian:20181204165558p:plain

細かいのはすっ飛ばして今回の箇所だけ。 (実はassetsのキャッシュ部分とか省略している)

# config.yml
version: 2
jobs:
  build:
    docker:
       ...

    steps:
      - checkout

      ...

      - run:
          name: assets:precompile
          command: |
              # bundlesizeで邪魔になるのでcacheされた古いassetsは削除する
              rm -rf public/assets public/packs-test
              bundle exec rake assets:precompile

      # bundlesizeで比較するためにdigestなしでファイルをコピーしておく
      - run: |
          cp public/assets/application{-*,}.js.gz
          cp public/assets/application{-*,}.css.gz

      - run: npx bundlesize

どんどん自動化していくでぇ〜 💪

以上です 🤗

CSS削除の前後でレイアウト崩れがないかどうかチェックするスクリプトを書いた

はじめに

「よっしゃあ、サイト高速化するぞ!」

「むむ、CSSサイズが大きいなぁ。軽量化しよう!」

「このCSS使われてないやん、、削除ー!」

・・・・・

他のページのレイアウト崩れてるやん 😇

CSSの構造が複雑になっていたので、削除の影響範囲わかんない。どうしよう。

ってことで、対応の前後でレイアウト崩れがないかどうかをチェックできるスクリプトをつくりました。

あんましnode使ったことなかったんですが、puppeteerでさくっと作れました。

流れとしては、

  1. 対応の前後でサイトのスクショを撮る
  2. スクショ画像の差分をチェックする

っていう手順です。

サイトのスクショを撮る

PATHSにスクショ撮影対象のパスを入れてます。
スマホサイトにするためにUserAgentも設定。
ディレクトリつくってそこに一括で画像保存してます。

// main.js

// iPhone X に合わせた
const DEFAULT_VIEWPORT = {
  width: 375,
  height: 812,
  deviceScaleFactor: 1,
};

const puppeteer = require('puppeteer');
const fs = require('fs');

const DEFAULT_HOST = 'https://qiita.com';

const PATHS = [
  '/',
  '/paranishian',
  '/tags/aws',
  '/tags/ruby'
]

const date = new Date();
const timestamp = date.getTime().toString();

async function run(){
  // ブラウザを起動する
  const browser = await puppeteer.launch({
    // headless: false,
    defaultViewport: DEFAULT_VIEWPORT,
  });

  // ページつくる
  const context = await browser.createIncognitoBrowserContext();
  const page = await context.newPage();

  // スマホのUserAgentにする
  await page.setUserAgent('Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)')

  // ディレクトリ作成
  fs.mkdirSync(timestamp);

  for(const path of PATHS) {
    const url = DEFAULT_HOST + path;
    await page.goto(url, {waitUntil: 'networkidle2'});

    console.log(`Done! : ${url}`);

    const filename = path.replace(/\//g, '_') + '.png';
    // スクリーンショットを保存
    await page.screenshot({path: timestamp + '/' + filename, fullPage: true})
  }

  // 終了
  await browser.close()
}

run()

さっそく実行。

❯ node main
Done! : https://qiita.com/
Done! : https://qiita.com/paranishian
Done! : https://qiita.com/tags/aws
Done! : https://qiita.com/tags/ruby

❯ ls -l 1542803191786
total 12144
-rw-r--r--  1 paranishian  www   657027 11 21 21:26 _.png
-rw-r--r--  1 paranishian  www   749702 11 21 21:26 _paranishian.png
-rw-r--r--  1 paranishian  www  2202719 11 21 21:26 _tags_aws.png
-rw-r--r--  1 paranishian  www  2599285 11 21 21:26 _tags_ruby.png

こんな感じで画像保存してます。

差分をチェックする

pixel-diffで差分をチェックします。

// diff.js

const DIFF_DIRECTORY = 'diff';

const fs = require('fs');
const PixelDiff = require('pixel-diff');
const PNG = require('pngjs').PNG;
const path = require('path');

const argv = require('yargs')
  .options({
    'a': {
      describe: '比較対象画像のディレクトリA',
      type: 'string',
      demandOption: true
    },
    'b': {
      describe: '比較対象画像のディレクトリB',
      type: 'string',
      demandOption: true
    },
  })
  .help()
  .argv;

async function checkDiff(filename) {

  // NOTE: 同じファイル名の画像がある前提なのでファイルの存在チェックはしていない
  const imageA = `${argv.a}/${filename}`;
  const imageB = `${argv.b}/${filename}`;
  const pngA = await PNG.sync.read(fs.readFileSync(imageA));
  
  const diff = new PixelDiff({
    imageAPath: imageA,
    imageBPath: imageB,
    thresholdType: PixelDiff.THRESHOLD_PERCENT, // thresholdType: PixelDiff.RESULT_DIFFERENT,
    threshold: 0.01, // 1% threshold
    imageOutputPath: `${DIFF_DIRECTORY}/${filename}`,
    cropImageB: {
      x: 0,
      y: 0,
      width: pngA.width,
      height: pngA.height,
    },
  });

  const result = await diff.runWithPromise();
  console.log(`Found ${result.differences} pixels differences in ${filename}.`);

}


async function run(){
  // diff保存用ディレクトリ作成
  if (!fs.existsSync(DIFF_DIRECTORY)){
    fs.mkdirSync(DIFF_DIRECTORY);
  }

  if (!fs.existsSync(argv.a) || !fs.existsSync(argv.b)){
    throw new Error('ディレクトリが見つかりません!');
  }

  fs.readdir(argv.a, (err, files) => {
    files.forEach(file => {
      // pngファイル以外は無視する
      if(path.extname(file) != '.png') {
        return;
      }

      checkDiff(file)
    });
  })
}

run()

引数に対象ディレクトリ名を指定して実行すると、diffディレクトリが作成されて差分画像が保存されます。

❯ node diff -a 12345 -b 67890
...

最終的にはCIに導入してmasterとの差分チェックを自動化するのが目標です。

それにしてもpuppeteerは簡単で最高ですね 🤤

どなたかの参考になれば幸いです 🤗

参考

アセットの変更がない場合、assets:precompileをスキップすることでCIの速度を改善した

f:id:paranishian:20181120150741p:plain

assets:precompileに毎回1分ほどかかっていたので嬉しい速度改善 😋

以下の記事を参考にさせていただきました 🙏
CircleCI 2.0に移行して新機能を活用したらCIの実行時間が半分になった話 - クラウドワークス エンジニアブログ

仕組みとしては、precompile対象のアセットの変更をgitのrevisionを使ってチェックして、変更があればprecompile、なければスキップする、という流れです。

自分のプロジェクトではwebpacker + react-railsを使ってるので以下を考慮しました。

  • compile対象のディレクトリ(app/javascript)の変更もチェックする
  • compile後のassets(public/packs-test)もキャッシュする

.circleci/config.yml

...
    steps:

      ...

      # precompile対象のassetsのgitのrevisionをkeyにする
      - run:
          name: create key for cashing assets
          command: |
            git rev-parse $(git log --oneline -n 1 app/assets lib/assets vendor/assets Gemfile.lock app/javascript | awk '{{print $1}}') > VERSION

      - restore_cache:
          keys:
            - asset-cache-{{ arch }}-{{ .Branch }}-{{ checksum "VERSION" }}
            - asset-cache-{{ arch }}-{{ .Branch }}
            - asset-cache

      # assetsのrevisionが変わってなければキャッシュから使う。変わっていればprecompile
      - run:
          name: assets:precompile
          command: |
            current_revision=VERSION
            previous_revision=public/assets/VERSION

            if [ ! -e $previous_revision ] || ! diff $previous_revision $current_revision; then
              bundle exec rake assets:precompile
              cp -f $current_revision $previous_revision
            else
              echo "Skipped."
            fi

      - save_cache:
          key: asset-cache-{{ arch }}-{{ .Branch }}-{{ checksum "VERSION" }}
          paths:
            - public/assets
            - public/packs-test
            - tmp/cache/assets/sprockets

どんどん改善していくでー 💪

参考