WebAssembly в браузере: где он ускоряет веб-приложения, а где бесполезен
Как работает WebAssembly, в каких задачах он оправдан, публичные примеры Figma, Photoshop и AutoCAD, готовые библиотеки вроде DuckDB-Wasm и ffmpeg.wasm, Rust и Emscripten, Web Worker, а также статус Wasm 3.0, WasmGC, JSPI и WASI 0.3.
WebAssembly, или Wasm, — это компактный бинарный формат, который браузер исполняет рядом с JavaScript почти со скоростью машинного кода. Он позволяет перенести в веб код на C, C++, Rust и других языках: графический редактор, САПР, видеоконвертер, аналитическую базу данных. Но это не замена JavaScript и не кнопка «сделать сайт быстрее»: в типичном интерфейсе с формами и запросами к API WebAssembly ничего не ускорит. Разберём, как WebAssembly устроен, в каких задачах он действительно даёт выигрыш, какие публичные примеры можно проверить, как встроить модуль в веб-приложение и что изменилось в стандарте за последние годы — от Wasm 3.0 до WASI 0.3. Как работает WebAssembly Модуль WebAssembly — это файл .wasm с инструкциями для стековой виртуальной машины. Браузер проверяет модуль, компилирует его в машинный код процессора и создаёт экземпляр с собственной линейной памятью. Всё это происходит в той же песочнице, что и JavaScript, с теми же ограничениями безопасности. Ключевые свойства, которые важно понимать до выбора технологии: Предсказуемая производительность. Код статически типизирован и компилируется заранее, поэтому скорость меньше зависит от прогрева JIT-компилятора и эвристик оптимизации, чем у JavaScript. Потоковая компиляция. Браузер может компилировать модуль, пока он ещё скачивается. Нет прямого доступа к DOM. Работа со страницей, сетью и большинством браузерных API идёт через JavaScript. Каждый переход через границу JavaScript и WebAssembly имеет цену. Много языков. Компиляторы в WebAssembly есть для C и C++ (Emscripten), Rust, Go, C#, Kotlin, Dart и других. Загрузка модуля // Сервер должен отдавать .wasm с Content-Type: application/wasm const { instance } = await WebAssembly.instantiateStreaming( fetch('/wasm/image-tools.wasm'), { env: { log: (value) = console.log('wasm:', value), }, } ); const { memory, alloc, grayscale } = instance.exports; Используйте instantiateStreaming , а не последовательность «скачать буфер — скомпилировать — создать экземпляр»: так компиляция идёт параллельно загрузке, а Chrome ещё и сохраняет скомпилированный код достаточно крупных модулей в своём кэше. Сохранять скомпилированные модули в IndexedDB вручную, как советовали старые руководства, больше нельзя: браузеры отказались от этой возможности в пользу встроенного кэширования. Где WebAssembly оправдан Практическое правило: WebAssembly даёт выигрыш, когда узкое место — вычисления на процессоре, а не сеть и не отрисовка интерфейса, и когда для задачи уже есть надёжная библиотека на C, C++ или Rust. Обработка изображений, аудио и видео прямо в браузере: сжатие, конвертация, фильтры, нарезка без загрузки файла на сервер. Работа с документами: рендеринг PDF, распознавание текста, разбор больших файлов Excel и CSV. Аналитика на клиенте: SQL-запросы к наборам данных в сотни мегабайт без отдельного аналитического API. Графические редакторы, САПР, 3D: перенос существующего нативного движка в браузер вместо переписывания на JavaScript. Машинное обучение на устройстве: инференс небольших моделей без отправки данных на сервер. Криптография и проверка подписей на стороне клиента. Общая логика для разных платформ: одно ядро расчётов для веба, мобильных приложений и сервера. Когда WebAssembly не нужен Интерфейс, формы, запросы к API. Время уходит на сеть и отрисовку, а не на вычисления. Частая работа с DOM. Каждое обращение идёт через JavaScript, и такой код оказывается медленнее чистого JavaScript. Мелкие вычисления. Затраты на загрузку модуля и переходы через границу съедают выигрыш. Жёсткий бюджет на размер первой загрузки. Модули с портированными библиотеками весят от сотен килобайт до десятков мегабайт. Нет готовой библиотеки и компетенций в системных языках. Переписывать алгоритм с JavaScript на Rust ради ускорения стоит только после профилирования, которое доказало, что проблема именно в вычислениях. Публичные примеры Утверждения вида «WebAssembly быстрее JavaScript в N раз» без условий замера мало что значат: результат зависит от задачи, объёма данных и того, насколько хорошо оптимизирован JavaScript-вариант. Надёжнее смотреть на задокументированные внедрения. Figma. Редактор написан на C++. В 2017 году компания рассказала в блоге, что после перехода с asm.js на WebAssembly время загрузки — инициализация приложения, загрузка и первая отрисовка документа — сократилось более чем втрое. Adobe Photoshop в браузере. Статья на web.dev описывает, что именно WebAssembly и Emscripten позволили Adobe перенести в веб существующую кодовую базу Photoshop, а не писать редактор заново. AutoCAD Web. Autodesk публично рассказывала на Google I/O и QCon в 2018 году, как с помощью WebAssembly перенесла в браузер код AutoCAD с тридцатилетней историей. TensorFlow.js. В блоге TensorFlow в 2020 году сообщалось, что WebAssembly-бэкенд работает в 10–30 раз быстрее бэкенда на чистом JavaScript на официальных моделях (замер в Chrome на MacBook Pro 2018 года). Там же оговорено, что для большинства моделей бэкенд на WebGL всё равно быстрее, а WebAssembly выигрывает на лёгких моделях и там, где WebGL недоступен. Squoosh. В открытом приложении для сжатия изображений от команды Chrome кодеки MozJPEG, libwebp и другие скомпилированы в WebAssembly, поэтому вся обработка идёт прямо в браузере. Общий знаменатель этих примеров — не «ускорение сайта», а возможность перенести в браузер тяжёлый нативный код, который раньше требовал установки программы. Готовые библиотеки Писать собственный модуль приходится реже, чем кажется: для большинства типовых задач есть проверенные порты. Задача Библиотека Видео и аудио ffmpeg.wasm Аналитические запросы, Parquet, CSV DuckDB-Wasm Локальная реляционная база SQLite Wasm — официальная сборка с поддержкой Origin Private File System Компьютерное зрение OpenCV.js Инференс моделей ONNX Runtime Web, TensorFlow.js Распознавание текста Tesseract.js Криптография libsodium.js Python в браузере Pyodide Приложения на C# и .NET Blazor WebAssembly Пример: аналитика в браузере на DuckDB-Wasm import * as duckdb from '@duckdb/duckdb-wasm'; const bundles = duckdb.getJsDelivrBundles(); const bundle = await duckdb.selectBundle(bundles); // Воркер с CDN подключается через Blob, чтобы обойти ограничение same-origin const workerUrl = URL.createObjectURL( new Blob([`importScripts("${bundle.mainWorker}");`], { type: 'text/javascript' }) ); const worker = new Worker(workerUrl); const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker); await db.instantiate(bundle.mainModule, bundle.pthreadWorker); URL.revokeObjectURL(workerUrl); const conn = await db.connect(); const result = await conn.query(` SELECT region, SUM(revenue) AS total FROM read_parquet('https://data.example.ru/sales.parquet') GROUP BY region ORDER BY total DESC `); console.log(result.toArray()); Такой подход подходит для внутренних дашбордов и BI-инструментов, где пользователю нужно гибко фильтровать и группировать выгрузку без запроса к серверу на каждое действие. Ограничения очевидны: данные скачиваются на устройство, поэтому конфиденциальные наборы и объёмы в гигабайты требуют другой архитектуры. Как строятся полноценные системы управленческой отчётности — в статье о BI-системах . На чём писать модули Rust Хороший выбор для нового кода: безопасная работа с памятью без сборщика мусора, компактный результат и развитые инструменты. Связку с JavaScript генерирует wasm-bindgen, сборку пакета — wasm-pack. В 2025 году рабочая группа Rust по WebAssembly закрыла свою организацию на GitHub: wasm-bindgen переехал в отдельную организацию с новыми сопровождающими, а wasm-pack передан одному из прежних мейнтейнеров. Инструменты продолжают развиваться, но при выборе версий стоит смотреть на новые репозитории. // src/lib.rs use wasm_bindgen::prelude::*; #[wasm_bindgen] pub fn grayscale(pixels: mut [u8]) { for px in pixels.chunks_exact_mut(4) { let y = (0.299 * px[0] as f32 + 0.587 * px[1] as f32 + 0.114 * px[2] as f32) as u8; px[0] = y; px[1] = y; px[2] = y; } } wasm-pack build --target web // main.js import init, { grayscale } from './pkg/image_tools.js'; await init(); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); grayscale(imageData.data); // весь буфер одним вызовом ctx.putImageData(imageData, 0, 0); Обратите внимание: изображение передаётся в модуль целиком, одним вызовом. Вызывать функцию WebAssembly для каждого пикселя — верный способ получить код медленнее, чем на JavaScript. Если команда только знакомится с языком, начните со статьи «Rust для JavaScript-разработчиков» . C и C++ через Emscripten Основной путь для переноса существующих библиотек и приложений. Emscripten эмулирует значительную часть POSIX-окружения, файловую систему и работу с графикой, поэтому именно так портированы SQLite, FFmpeg, OpenCV и крупные коммерческие продукты. Цена — более тяжёлый результат и сложная настройка сборки. Языки со сборщиком мусора С появлением WasmGC в WebAssembly компилируются Kotlin, Dart и другие языки с управляемой памятью без необходимости тащить в модуль собственный сборщик мусора. Flutter умеет собирать веб-приложения в WebAssembly, у Kotlin есть целевая платформа Kotlin/Wasm. Для C# есть Blazor WebAssembly, для Go — стандартный компилятор и TinyGo, который даёт модули заметно меньшего размера. AssemblyScript Язык с синтаксисом, похожим на TypeScript, который компилируется в WebAssembly. Порог входа для фронтенд-команды ниже, но экосистема и возможности заметно скромнее, чем у Rust и C++. Архитектура: как встроить модуль Вычислительное ядро WebAssembly отвечает за тяжёлые вычисления, JavaScript — за интерфейс, события и сеть. Данные передаются крупными блоками через общую память, количество вызовов через границу минимально. Web Worker Любая операция, которая может занять заметное время, выполняется в Web Worker. Иначе модуль заблокирует главный поток, и интерфейс перестанет откликаться — это напрямую ухудшит метрику INP. // worker.js import init, { processRows } from './pkg/compute.js'; const ready = init(); // инициализация один раз при старте воркера self.onmessage = async (event) = { await ready; const result = processRows(event.data.rows); // Float64Array self.postMessage(result, [result.buffer]); // передача без копирования }; // main.js const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }); worker.onmessage = (event) = renderChart(event.data); worker.postMessage({ rows: dataset }); Ленивая загрузка Модуль не должен входить в первую загрузку страницы. Подгружайте его, когда пользователь открывает редактор, нажимает «Конвертировать» или переходит в раздел аналитики. Для многопоточных сборок учтите требование изоляции: SharedArrayBuffer доступен только при заголовках Cross-Origin-Opener-Policy и Cross-Origin-Embedder-Policy, а они влияют на встраивание сторонних виджетов и ресурсов. Ограничения и подводные камни Отладка. Chrome DevTools умеют отлаживать C, C++ и Rust по отладочной информации DWARF, но для этого нужна специальная сборка и расширение. Ошибки в продакшене разбирать сложнее, чем в JavaScript. Память. Линейная память модуля может расти, но не уменьшается. В C и C++ утечки — ответственность разработчика, и в долгоживущем приложении они накапливаются. Размер. Порты крупных библиотек весят мегабайты. Нужны сжатие Brotli, кэширование и загрузка по требованию. Безопасность. Песочница браузера защищает систему пользователя, но не спасает от уязвимостей внутри самой библиотеки. Портированный C-код с переполнением буфера останется уязвимым, пусть и в пределах своей памяти, поэтому библиотеки нужно обновлять так же, как серверные зависимости. Что изменилось в стандарте WebAssembly 3.0 В сентябре 2025 года рабочая группа объявила о завершении спецификации Wasm 3.0. В стандарт вошли возможности, которые долго существовали в виде предложений: сборка мусора (WasmGC), 64-битная адресация памяти (Memory64), несколько блоков памяти в одном модуле, обработка исключений, хвостовые вызовы, расширенный SIMD. Для веб-разработки самое заметное — WasmGC: Chrome поддерживает его с версии 119, Firefox — с