Profile Picture

Measuring Bundle Sizes with Next.js and GitHub Actions, Part 2

May 15th, 2021

I wrote Part 1 of this post back in February, demonstrating how to measure Next.js bundle sizes with GitHub Actions. Today, I’ll walk through how to show a bundle size diff against master:

We’ll start from the final code from part 1, which comments your project’s bundle sizes on every PR action and push to master. The first thing we need to do is grab something to compare against: we’ll use whatever is currently on master.

Since we’re running our Action on every push to master, we don’t need to recalculate master’s bundle size—we can just use the previously calculated sizes! For this, we can use the Action action-download-artifact:

- name: Download master JSON
  uses: dawidd6/action-download-artifact@v2
  if: success() && github.event.number
  with:
    workflow: bundle-size.yml
    branch: master
    path: .next/analyze/master

This downloads all artifacts from the most recent run of our Action workflow on master into the folder .next/analyze/master. Note that Part 1’s Build & analyze step creates the folder .next/analyze/master—this is necessary for this step to succeed.

Now, we can write a script that compares the bundle.json files across master and our new PR:

const currentBundle = require("../.next/analyze/bundle.json");
const masterBundle = require("../.next/analyze/master/bundle/bundle.json");

const sizes = currentBundle
  .map(({ path, size }) => {
    const masterSize = masterBundle.find(x => x.path === path);
    // if a file exists in our bundle but not master's, it was added
    const diff = masterSize ? size - masterSize.size : "added";
  })
  // if a file exists in master's bundle but not ours, it was removed
  .concat(
    masterBundle
      .filter(({ path }) => !currentBundle.find(x => x.path === path))
      .map(({ path }) => "removed")
  );

Then, using the same code from part 1, we can output a Markdown table of this diff to publish to a GitHub comment:

const fs = require("fs");
const path = require("path");

const currentBundle = require("../.next/analyze/bundle.json");
const masterBundle = require("../.next/analyze/master/bundle/bundle.json");

const prefix = ".next";
const outdir = path.join(process.cwd(), prefix, "analyze");
const outfile = path.join(outdir, "bundle-comparison.txt");

function formatBytes(bytes, signed = false) {
  const sign = signed ? (bytes < 0 ? "-" : "+") : "";
  if (bytes === 0) return `${sign}0B`;

  const k = 1024;
  const dm = 2;
  const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];

  const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));

  return `${sign}${parseFloat(Math.abs(bytes / Math.pow(k, i)).toFixed(dm))}${
    sizes[i]
  }`;
}

const sizes = currentBundle
  .map(({ path, size }) => {
    const masterSize = masterBundle.find(x => x.path === path);
    const diffStr = masterSize
      ? formatBytes(size - masterSize.size, true)
      : "added";
    return `| \`${path}\` | ${formatBytes(size)} (${diffStr}) |`;
  })
  .concat(
    masterBundle
      .filter(({ path }) => !currentBundle.find(x => x.path === path))
      .map(({ path }) => `| \`${path}\` | removed |`)
  )
  .join("\n");

const output = `# Bundle Size
| Route | Size (gzipped) |
| --- | --- |
${sizes}
<!-- GH BOT -->`;

try {
  fs.mkdirSync(outdir);
} catch (e) {
  // may already exist
}

fs.writeFileSync(outfile, output);

The last step is to include this script in our Actions workflow:

# Place this after "Download master JSON"
- name: Compare bundle size
  if: success() && github.event.number
  run: ls -laR .next/analyze/master && node scripts/compare-bundles.js

# Modify this step to use `bundle-comparison.txt`, the new file we're uploading
- name: Get comment body
  id: get-comment-body
  if: success() && github.event.number
  run: |
    body=$(cat .next/analyze/bundle-comparison.txt)
    body="${body//'%'/'%25'}"
    body="${body//$'\n'/'%0A'}"
    body="${body//$'\r'/'%0D'}"
    echo ::set-output name=body::$body

If all goes well, you’ll see a bundle size diff in your PR!

Enjoyed this post? Follow me on Twitter for more content like this. Or, subscribe to my email newsletter to get new articles delivered straight to your inbox!

Related Posts

Monotonic Last Modified Columns in Postgres
Seamless Migration Squashing for EF Core 6 Migration Bundles
Visualizing and Deleting Entity Hierarchies in EF Core
Scroll to top