A Journey to Modernization Link to heading
Recently, while updating my resume, I took a critical look at my WebShot project. The codebase, written in TypeScript with Express for the backend and EJS/SCSS for the frontend, felt outdated compared to modern technologies. While updating was an option, I decided a complete refactor was the best path forward.
NestJS: A Promising Start Link to heading
NestJS immediately caught my eye. Its elegance, rich features, and native TypeScript support made it a strong contender. I started by creating a new project using the NestJS CLI and wrote the controllers with enthusiasm.
However, I encountered a hurdle when it came to serving the WebShot frontend. While NestJS supports template rendering, it wouldn’t handle my specific needs. My frontend relied on SCSS and TypeScript, requiring a bundler like Webpack or Vite for effective asset management.
Having wrestled with bundlers in the past, the prospect of significant time and effort investment became a deal-breaker. Regretfully, I decided to move on from NestJS and explore other options.
NuxtJS Link to heading
As a Vue.js fan, Nuxt.js, a full-stack framework with built-in server-side rendering (SSR), became my top choice. I utilized the Nuxt CLI to establish a new project and began development. Nuxt’s server-side rendering hinges on a component called Nitro, previously part of Nuxt itself.
Nitro acts as a standalone webserver for JavaScript runtimes like Node.js.
I effortlessly integrated Prisma, Sharp, and Puppeteer to construct the application.
Nitro’s documentation had some gaps, but I was able to find the info I needed with a little extra digging.
Concurrency Link to heading
Initially, capturing screenshots might seem like a simple, multi-step process:
- Launch a browser using Puppeteer.
- Open a tab and navigate to the user-provided URL.
- Capture a screenshot and return it.
- Close the tab and browser.
However, this approach crumbles in a multi-user, high-traffic scenario. Multiple users might request screenshots of different or even identical URLs. Launching and closing the browser for each capture is inefficient. To optimize resource usage, the browser instance needs to be shared.
Here’s where concurrency challenges arise:
In this scenario, Bob receives an unintended image because his tab activation overlaps with Alice’s capture. To address this, we need to make the capture process atomic. I implemented a locking mechanism. During tab activation and capture, the browser is locked. Once the promise resolves, the lock is released, preventing other requests from interfering.
Error Handling Link to heading
Another backend hurdle involved Satori, a library used for generating error messages. Since WebShot serves screenshots directly within HTML tags or Javascript programs, traditional JSON responses wouldn’t suffice. The errors needed to be embedded within the image itself.
Satori, with its magic (the specifics of which remain a mystery to me!), creates error messages mimicking Google Chrome’s style. As I was using a Vue-based framework, I opted for v-satori, a Vue adapter for Satori. v-satori leverages Vue’s built-in SSR to transpile components into HTML, which satori-html then processes.
A challenge arose when incorporating local images. To display a local image, its content needs base64 encoding and insertion into the tag’s src attribute.
Otherwise satori going to fetch the image from the internet.
The problem was importing an image in nuxt is not easily possible.
Nuxt relays on <NuxtImage />
component for handling images and this was a edge sitution which was not support.
Through investigation, I discovered Nuxt utilizes Vite for the frontend and Rollup for Nitro (the backend engine).
Both configurations can be customized within the nuxt.config.ts
file.
Fortunately, Rollup offers a plugin named @rollup/plugin-image that allows image importing as base64 strings.
import image from '@rollup/plugin-image';
export default defineNuxtConfig({
nitro: {
rollupConfig: {
plugins: [
image()
]
}
}
}
You can access to complete source code on Github: nuxt.config.ts
<template>
<img :src="img" :width="74" :height="74" />
</template>
<script>
import img from "@/assets/images/error/dinasor.png";
export default defineComponent({
setup() {
return { img };
},
});
</script>
You can access to complete source code on Github: ErrorImage.vue
This blog post is the story of WebShot’s makeover: choosing Nuxt.js, conquering concurrency challenges, and mastering error handling with Satori.
It wasn’t always smooth sailing, but the lessons learned were epic!