JavaScript обычно выполняется в одном потоке, который часто называют главным. Это означает, что JavaScript выполняет одну задачу за раз в синхронном режиме. Главный поток также обрабатывает задачи рендеринга — рисование и верстку, — а также взаимодействие с пользователем. Таким образом, длительное выполнение задач JavaScript иногда приводит к тому, что браузер перестает реагировать на запросы. Именно поэтому веб-страницы могут «зависать» при выполнении тяжелых функций JavaScript, блокируя взаимодействие с пользователем.
Мы продемонстрируем, как заблокировать главный поток, смоделировав тяжелые вычисления с использованием алгоритма Фибоначчи, и решим проблему заблокированного главного потока, используя следующие подходы:
- многопоточный (Web Worker);
- WebAssembly с AssemblyScript;
- WebAssembly с Rust.
Алгоритм Фибоначчи
Для всех примеров в этой статье будем использовать простой и довольно распространенный алгоритм Фибоначчи с временной сложностью O(2^n).
const calculateFibonacci = (n: number): number => {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};
Единый поток
Теперь реализуем алгоритм Фибоначчи непосредственно в главном потоке. Просто вызовем функцию Fibonacci при нажатии на кнопку.
"use client";
import { useState } from "react";
/**
* моделирование анимации загрузки
*/
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = useState<number | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const calculateFibonacci = (n: number): number => {
if (n <= 1) return n;
return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
};
const handleCalculate = () => {
setIsLoading(true);
/**
* моделирование длительного вычисления
*/
const result = calculateFibonacci(42);
setResult(result);
setIsLoading(false);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"
>
Calculate Fibonacci
</button>
{isLoading ? <Spinner /> : <p className="text-xl">Result: {result}</p>}
</div>
);
}
Теперь попробуем нажать кнопку «Рассчитать Фибоначчи» (Calculate Fibonacci) при измерении производительности. Чтобы измерить производительность кода, можно использовать инструменты оценки производительности в Chrome DevTools.
Как вы можете видеть из демонстрации пользовательского интерфейса, кнопка загрузки даже не появляется — вместо этого внезапно отображается результат вычисления. Инструменты оценки производительности показывают, что анимация загрузки блокируется тяжелыми вычислениями алгоритма Фибоначчи в главном потоке примерно на 2,06 секунды.
Многопоточность (Web Worker)
Распространенным подходом для разгрузки основного потока от тяжелых вычислений является использование веб-воркера (Web Worker).
/**
* перемещение алгоритма Фибоначчи на веб-воркер
*/
self.addEventListener("message", function (e) {
const n = e.data;
const fibonacci = (n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};
const result = fibonacci(n);
self.postMessage(result);
});
"use client";
import { useState } from "react";
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = (useState < number) | (null > null);
const [isLoading, setIsLoading] = useState < boolean > false;
/**
* вместо того, чтобы запускать функцию Fibonacci в главном потоке,
* запустим ее в веб-воркере
*/
const handleCalculate = () => {
setIsLoading(true);
const worker = new Worker(
new URL("./fibonacci-worker.js", import.meta.url),
);
worker.postMessage(42);
worker.onmessage = (e) => {
setResult(e.data);
setIsLoading(false);
worker.terminate();
};
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"
>
Calculate Fibonacci
</button>
{isLoading ? <Spinner /> : <p className="text-xl">Result: {result}</p>}
</div>
);
}
Если мы проведем измерение теперь, то окажется, что анимация загрузки работает гладко. Это происходит потому, что мы переложили тяжелые вычисления на поток воркера, избежав блокировки основного потока.
Как видите, и однопоточные вычисления, и вычисления с помощью воркера занимают одинаковое время — около 2 секунд. Как улучшить ситуацию? Ответ заключается в использовании WebAssembly.
WebAssembly — AssemblyScript
Фронтенд-инженер, имеющий ограниченный опыт работы с другими языками и желающий попробовать WebAssembly, скорее всего, выберет AssemblyScript, потому что он предлагает опыт, наиболее близкий к TypeScript.
Вот эквивалентный код Фибоначчи, написанный на AssemblyScript:
export function fibonacci(n: i32): i32 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
Если мы скомпилируем этот код, то будет создан файл release.wasm
. Затем можно использовать этот Wasm-файл в кодовой базе JavaScript.
"use client";
import { useState } from "react";
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = (useState < number) | (null > null);
const [isLoading, setIsLoading] = useState < boolean > false;
const handleCalculate = async () => {
setIsLoading(true);
// Загрузите и инстанцируйте модуль WebAssembly
const wasmModule = await fetch("/release.wasm");
const buffer = await wasmModule.arrayBuffer();
const module = await WebAssembly.instantiate(buffer);
const wasm = module.instance.exports;
// Вызовите функцию Фибоначчи из модуля WebAssembly
const fibResult = wasm.fibonacci(42);
setResult(fibResult);
setIsLoading(false);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"
>
Calculate Fibonacci
</button>
{isLoading ? <Spinner /> : <p className="text-xl">Result: {result}</p>}
</div>
);
}
Теперь, если мы проведем повторное измерение, анимация загрузки появится и не будет блокироваться тяжелыми вычислениями несмотря на то, что мы все еще находимся в главном потоке. Алгоритм Фибоначчи теперь занимает около 950 мс, что на 53 % быстрее, чем при использовании только JavaScript.
WebAssembly — Rust
Rust — один из популярных вариантов для WebAssembly, что подчеркивается в официальной документации Mozilla. Попробуем реализовать тот же алгоритм Фибоначчи, но написанный на Rust.
use wasm_bindgen::prelude::*;
// Передайте функцию в JavaScript с помощью WebAssembly
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
"use client";
import { useState } from "react";
function Spinner() {
return (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue-500"></div>
</div>
);
}
export default function Home() {
const [result, setResult] = (useState < number) | (null > null);
const [isLoading, setIsLoading] = useState < boolean > false;
const handleCalculate = async () => {
setIsLoading(true);
// Загрузите и инстанцируйте модуль WebAssembly
const wasmModule = await fetch("/pkg/rust_wasm_fibonacci_bg.wasm"); // Use the actual wasm file
const buffer = await wasmModule.arrayBuffer();
const module = await WebAssembly.instantiate(buffer);
const wasm = module.instance.exports;
// Вызовите функцию Фибоначчи из модуля WebAssembly
const fibResult = wasm.fibonacci(42); // Предполагается, что функция экспортируется как 'fibonacci'
setResult(fibResult);
setIsLoading(false);
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-900 text-white">
<button
onClick={handleCalculate}
className="mb-8 px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition"
>
Calculate Fibonacci
</button>
{isLoading ? <Spinner /> : <p className="text-xl">Result: {result}</p>}
</div>
);
}
Теперь рассмотрим результат использования WebAssembly с Rust. Мы по-прежнему задействуем главный поток, но теперь уже с Wasm. Как и в случае с AssemblyScript, даже если мы запускаем Wasm в основном потоке, анимация загрузки все равно появляется и не блокируется. Удивительно то, что эти тяжелые вычисления теперь занимают всего 684 мс, что на 66 % быстрее, чем при использовании только JavaScript.
TL;DR: основные тезисы
- Тяжелые вычисления блокируют основной поток и останавливают все виды анимации.
- Тяжелые вычисления можно переложить на веб-воркер.
- Скорость тяжелых вычислений можно оптимизировать, переписав логику на WebAssembly. Используя алгоритм Фибоначчи в качестве примера, мы получили следующие результаты:
— JavaScript: 2 с;
— WebAssembly — AssemblyScript: 953 мс (на 53 % быстрее, чем с JavaScript);
— WebAssembly — Rust: 684 мс (на 66 % быстрее, чем с JavaScript).
Читайте также:
- Введение в WebAssembly (WASM)
- Реальные возможности WASM
- Надоело работать во фронтенд с JavaScript? Используйте Python для браузера!
Читайте нас в Telegram, VK и Дзен
Перевод статьи Titus Efferian: Rust vs JavaScript: Achieving 66% Faster Performance with WebAssembly