Creating PDF Files Without Slowing Down Your App
Tags: react, webdev, typescript
Published: 2023-02-19 14:29:44

In many web applications, there are times when we need to generate and download PDF files on the client-side. However, for the sake of showing a live preview to users, PDF generation may block the main thread and cause poor user experience. This is where web workers come into play. In this article, we will explore how to use [**@react-pdf/renderer**](https://react-pdf.org/) inside a [**web worker**](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API?retiredLocale=de) to render PDF files in a non-blocking way.

## What is react-pdf?
[**@react-pdf/renderer**](https://react-pdf.org/)  is an open-source library that enables developers to create and render PDF documents using React components, which allows developers to use a familiar declarative syntax. @react-pdf/renderer provides a set of built-in components for creating basic PDF elements such as text, images and links. It’s engine-agnostic and can run both inside the browser and NodeJS.

## What are Web Workers?
A [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API?retiredLocale=de) is a JavaScript that runs in the background, separate from the main thread. It allows us to perform long-running tasks without blocking the main thread, which can improve the overall performance and user experience of an application. Web workers can communicate between each other and the main thread via messages. Web worker should primarily be used for long-running tasks, as managing web workers and sending data between them has performance implications.

## Final Result

Checkout the repo [here](https://github.com/shkreios/vite-react-pdf-renderer-web-worker) or the stackblitz demo [here](https://stackblitz.com/github/shkreios/vite-react-pdf-renderer-web-worker)

{% stackblitz https://stackblitz.com/edit/vite-react-ts-zbwjtr?embed=1&file=README.md&view=preview %}

## How it all comes together?

Before we get into the details of using web workers, let’s start by creating a simple react pdf component. Here’s an example:

```tsx
import { Page, Document, Text } from '@react-pdf/renderer';

export type PDFProps = {
  title: string;
  author: string;
  description: string;
};

export const PDF = (props: PDFProps) => (
  <Document title={props.title} author={props.author} subject={props.description}>
    <Page>
      <Text>Hello, World!</Text>
    </Page>
  </Document>
);
```

This component creates a PDF document with a single page that says, “Hello, World!”.

### Creating a RenderPDF Function

Now that we have a PDF component, we need a way to convert it to a PDF file. We can do this using the `toBlob` method of the `pdf` function provided by `@react-pdf/renderer`. Here’s an example of a function that does this:
```ts
import { createElement } from 'react';
import type { PDFProps } from './PDF';

export const renderPDF = async (props: PDFProps) => {
  const { pdf } = await import('@react-pdf/renderer');
  const { PDF } = await import('./PDF');
  return pdf(createElement(PDF, props)).toBlob();
};
```
This function takes a PDFProps object as an argument, creates a PDF element using `createElement`, and then passes it to the `pdf` function provided by `@react-pdf/renderer`. Finally, it returns a Blob object representing the PDF file.

### Creating a Web Worker

Now that we have a way to create PDF files, we can create a web worker to run the `renderPDF` function in a separate thread. Here’s an example of a web worker that uses [**comlink**](https://github.com/GoogleChromeLabs/comlink) to expose the `renderPDF` function as an asynchronous function:
```ts
import { expose } from 'comlink';
import type { PDFProps } from '../PDF';
import './workerShim';
let log = console.info;

const renderPDFInWorker = async (props: PDFProps) => {
  try {
    const { renderPDF } = await import('../renderPDF');
    return URL.createObjectURL(await renderPDF(props));
  } catch (error) {
    log(error);
    throw error;
  }
};

const onProgress = (cb: typeof console.info) => (log = cb);

expose({ renderPDf, onProgress });

export type WorkerType = {
  renderPDf: typeof renderPDf;
  onProgress: typeof onProgress;
};
```

This code imports the renderPDF() function we created earlier, and exposes the `renderPDFInWorker` as an asynchronous function, which returns the blob URL using comlink. The `onProgress` function is used for debugging purposes to pipe the output back to main thread console.

## Integrating it into the application

By now having this simple async function, there is no requirement to use React as the main thread ui framework. Theoretically, it could be used in a jQuery or vanilla JS application.

The react implementation would look like this:
```ts
import { useEffect } from "react";
import { useAsync } from "react-use";
import { proxy, wrap } from "comlink";
import type { WorkerType } from "./workers/pdf.worker";
import Worker from "./workers/pdf.worker?worker";

export const pdfWorker = wrap<WorkerType>(new Worker());
pdfWorker.onProgress(proxy((info: any) => console.log(info)));

export const useRenderPDF = ({
  title,
  author,
  description,
}: Parameters<WorkerType["renderPDFInWorker"]>[0]) => {
  const {
    value: url,
    loading,
    error,
  } = useAsync(async () => {
    return pdfWorker.renderPDFInWorker({ title, author, description });
  }, [title, author, description]);

  useEffect(() => (url ? () => URL.revokeObjectURL(url) : undefined), [url]);
  return { url, loading, error };
};
```

To import the PDF worker, this example uses [vite’s query suffixes import syntax](https://vitejs.dev/guide/features.html#import-with-query-suffixes). After that, the worker is wrapped by comlink.

The `useRenderPDF` hook now takes in the PDF component props as its argument. It then uses the `useAsync` hook to call the `renderPDFInWorker` function, passing the component props to it and returning a value, error and loading state.

Finally, the `useEffect` hook is used to clean up the URL object created by the `renderPDFInWorker` function to prevent memory leaks.

## Improvements & Considerations

To minimize communication overhead, it was explicitly opted to send the component props instead of the complete v-dom, and thus this implementation is not as flexible as the non web worker implementation.

A potential improvement could therefore be to make it easier to use `renderPDFInWorker` with multiple different PDFs via the use of the factory pattern and inline workers.

The implementation was only tested in vite and required shims to make `@react-pdf/renderer` work inside a web worker, which may need to be adapted to work in other bundlers

In vites current setup, it doesn’t allow even ESM web workers to share code chunks with the main bundle. Thus, the bundle will include `@react-pdf/renderer`, react and all other libraries that both the PDF component and the main bundle uses twice.

## Conclusion

In this article, we’ve explored how to use `@react-pdf/renderer` inside a web worker to generate PDF files in a non-blocking way. We’ve used the comlink library to communicate with the worker and create a promise-based API wrapper.