Тяжёлые вычисления в Next.js
Данная статья описывает способ внедрения WebAssembly, выполняющегося в веб-воркере, в Next.js приложение.
Настройка Next.js
worker-loader - позволяет подключать в приложение файл с кодом веб-воркера при помощи стандартного импорта.
Следующие моменты при настройке worker-loader являются ключевыми:
- publicPath — должен быть правильным URL-ом, т.е. вида https://mysite/_next/, потому как import в веб-воркере трансформируется в importScript, требующий корректный URL.
- chunkFilename и filename должны иметь корректный префикс, чтобы комилируемые файлы сохранялись в static директорию Next.js.
- нужно включить в цепочку лоадеров стандартный Next.js лоадер для JavaScript/TypeScript.
Пример next.config.js с учётом вышеперечисленных моментов:
// next.config.jsconst workerLoader = require('worker-loader');module.exports = {webpack(config = {}, context) {const { isServer, defaultLoaders, config: nextConfig } = context;const publicPrefix = process.env.NEXT_PUBLIC_URL || nextConfig.assetPrefix;const publicPath = (publicPrefix ? `${publicPrefix}/` : '') + '_next/';const outputPrefix = (isServer ? '../' : '') + 'static/worker/';const workerLoaderOptions = {publicPath,chunkFilename: outputPrefix + 'chunks/[id].[contenthash].worker.js',filename: outputPrefix + '[name].[contenthash].worker.js',};config.modules.push({test: /\.worker\.(js|ts)$/,use: [{loader: 'worker-loader',options: workerLoaderOptions,},(defaultLoader: defaultLoaders.babel),],});return config;},};
В конфигурации используется NEXT_PUBLIC_URL переменная окружения, которая должна содержать URL текущего деплоя.
Имена веб-воркер файлов должны оканчиваться на .worker.ts или .worker.js.
Внутри веб-воркера
// heavy-calculation.worker.tsself.addEventLinstener('message', async ({ data }) => {const { payload, command } = data;if (command !== 'calculate') {return;}// (1)const { calculate } = await import('./wasm/heavy-calculation');const result = calculate(payload);self.postMessage({result,});});
Веб-воркер подписывается на сообщения, и в случае, если данные сообщения содержат поле command со значением calculate, вызывает WebAssembly функцию и отправляет сообщение с результатом вычисления.
Важным моментов является динамический импорт (1) wasm-файла. Стандартным решением было бы использовать статический импорт в начале файла, но данный подход приводит к ошибке — Next.js не может включить wasm-файл при серверном рендеринге или при генерации статической страницы. В то время как динамический импорт будет выполнен только в браузере.
Подключение веб-воркера
Создадим React компонент, задачами которого будут запуск вычисления в веб-воркере и отображение результата.
// HeavyCalculator.tsxtype Calculate = (n: number) => number;type WorkerMessageListener = (ev: WorkerEventMap['message']) => void;function HeavyCalculator() {const [result, setResult] = useState<number | null>(null);const [inputValue, setInputValue] = useState<stirng>();const worker = useRef<Worker | null>(null);const calculate = useRef<Calculate | null>(null);const listener = useRef<Listener | null>(null);useEffect(() => {if (calculate.current) {return;}// (1)import('./heavy-calculation.worker').then(({ default: HeavyCalculationWorker }) => {// (2)worker.current = new HeavyCalculationWorker();// (3)calculate.current = n => {worker.postMessage({command: 'calculate',payload: inputValue,});};// (4)listener.current = message => {if (mesage.data && typeof mesage.data.result !== 'undefined') {setResult(message.data.result);}};worker.current.addEventListener('message', listener);});() => {if (worker.current) {worker.current.removeEventListener('message', listener);}};}, []);function onCalculateClick() {if (calculate.current) {calculate.current(parseInt(inputValue, 10));}}return (<div><label htmlFor="heavy-calculator-input">Input data for heavy calculation</label><inputid="heavy-calculator-input"value={inputValue}onChange={e => {setInputValue(e.target.value);}}></input><button onClick={onCalculateClick}>calculate</button><div>{result}</div></div>);}
(1) — динамически импортируем веб-воркер на клиенте (попытка использовать статический импорт в начале файла приведёт к ошибке при серверном рендеринге).
(2) — инстанциируем веб-воркер и сохраняем его в ref-переменную, чтобы он был доступен вне колбэка импорта.
(3) — создаём функцию calculate, которая отправляет сообщение c необходимыми данными веб-воркеру, созданному на этапе (2).
(4) — подписываемся на события, приходящие от веб-воркера, у которых есть поле data.result.
Заключение
Данная реализация позволяет подключать на любую Next.js страницу React компонент, взаимодействующий с WebAssembly кодом, выполняющимся в веб-воркере. Как бонус — при создании нескольких экземпляров компонента для каждого из них будет инстанциирован свой веб-воркер, что позволяет, например, запустить несколько параллельных потоков вычислений.