Made in Builder.io

Ship Your First Personalized Web Experience webinar on June 15 @ 10AM PT. Register Now

Talk to Us
Product
Developers
Talk to Us

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web Development

Pixel Perfect Frontend Migration with SSDiff

April 18, 2023

Written By Akshat Garg

Migrating a frontend app can be a complex and challenging process, especially when moving from a React-based framework like Next.js to a non-React-based framework like Qwik.

At Builder.io, we have recently undertaken the task of migrating our documentation from Next.js to Qwik with the goal of achieving a high-performance website while maintaining the same look and functionality.

The challenge was that we have 150+ documents with 15+ custom registered components, and a successful migration requires a visual comparison of each page in both Next.js and Qwik versions.

In this blog post, we discuss our approach to accelerating testing for the migration with SSDiff, an open-source tool we have developed for pixel-perfect website migrations.

SSDiff: Pixel diff at scale

To automate the process of visually comparing the Next.js and Qwik versions of the documentation site, we needed a tool that could take screenshots of each page in both versions, run a diffing algorithm to identify differences, and provide a quantitative value for the difference.

The tool should also generate diffing images that highlight the components causing the difference. This should be done in parallel for multiple pages in batches, allowing us to identify pages with the most significant differences and prioritize them.

This approach would eliminate the need for manual side-by-side comparisons of web pages and streamline the issue detection process.

We initially developed a basic script to automate the process of opening web pages and taking screenshots for comparison between the Next.js and Qwik versions. We incorporated pixelmatch, an image-diffing library, to compare the screenshots and output the diffing files in a designated folder. To ensure better developer experience and enable type safety, we added TypeScript support to the project from the early stages.

// Calculate the diff between url1 and url2 and store it in a file
async compare(compareObj: { url1: string; url2: string; fileName: string }) {
    const { url1, url2, fileName } = compareObj;
    const [image1, image2] = await Promise.all([this.screenshot(url1), this.screenshot(url2)]);
    const maxHeight = Math.max(image1.height, image2.height);
    const maxWidth = Math.max(image1.width, image2.width);
		/* ...resize images if the dimensions are not same  */
    const numDiffPixels = pixelmatch(image1.data, image2.data, diff.data, maxWidth, maxHeight, pixelMatchConfig);
    const totalPixels = diff.data.length / 4;
    const differencePercentage = (numDiffPixels / totalPixels) * 100;
    this.fileNameDifferenceMap.set(fileName, differencePercentage); 
    fs.writeFileSync(this.diffScreenshots + `/${fileName}`, PNG.sync.write(diff));
}

As we tested more pages, we decided to change the output format of the tool to a sorted map, with URL path-names as keys and quantitative diffing values as corresponding values.

async sortFilesBasedOnDifference() {
    // sorts the map based on diffing values for fileNames as keys
    const sortedMap = new Map([...this.fileNameDifferenceMap.entries()].sort((a, b) => b[1] - a[1]));
    return sortedMap;
}

We also encountered an issue with the image-diffing tool where it would fail if the images being compared were of different sizes. To address this issue, we used sharp to resize images if they were of different sizes before comparison.

// resize and place image to top left of the canvas
async resizeImage(image: PNG, width: number, height: number) {
	  const sharpImage = sharp(image.data, { raw: { width: image.width, height: image.height, channels: 4 } });
	  const resizedImageBuffer = await sharpImage
	    .resize({
	      height,
	      width,
	      fit: 'contain',
	      position: 'left top',
	    })
	    .toFormat('png')
	    .toBuffer();
	  return PNG.sync.read(resizedImageBuffer);
  }

The resulting tool, named SSDiff, is an evolving project with potential for further extensions and use cases. It is currently available on npm and can be used to test different versions of a website.

Pixel diffing, a method used for identifying differences in components, has its own set of limitations and challenges, including:

  • Testing Stateful Components: Components that are affected by state changes may render different content based on the values of their state. The tool currently does not handle mutations and access to state automatically. Users may need to handle these programmatically or manually to cover all behaviors for a particular page or component.
  • Performance Bottlenecks: The tool uses Puppeteer to open a URL and capture a screenshot. When multiple pathnames are provided for testing, the tool spawns multiple pages in a browser. This may result in performance bottlenecks, which are typically observed when around 20 pathnames are provided for a given URL. However, it's worth noting that this performance may vary depending on the system. To obtain results for more than 20 pathnames in a single run, users may need to run the diffing process in batches.

A successful frontend migration should result in a new version of the site that looks identical to the original, but with improved performance and expected enhancements. The SSDiff tool we developed helped us identify areas for improvement and make our components more similar to the original site. While the tool has limitations in terms of speed and pinpointing the exact component causing visual differences, it has proved useful in streamlining our migration process.

Visually build with your components

Builder.io is a headless CMS that lets you drag and drop with your components right within your existing site.

// Dynamically render your components
export function MyPage({ json }) {
  return <BuilderComponent content={json} />
}

registerComponents([MyHero, MyProducts])

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Builder.io is a headless CMS that lets you build visually with your components.

Learn more

Like our content?

Join Our Newsletter

Continue Reading
Web Development11 MIN
Next.js 13 - Routing Fundamentals and Beyond
WRITTEN BYVishwas Gopinath
June 9, 2023
Latest News7 MIN
Builder Drop: More insights, environments and localization permissions
WRITTEN BYBuilder Team
June 8, 2023
Latest News6 MIN
The Dev Drop: Multi-threading in JSX, Resumability, Qwik Case Study, React in 2023
WRITTEN BYYoav Ganbar
June 1, 2023