JavaScript обычно выполняется в одном потоке, который часто называют главным. Это означает, что JavaScript выполняет одну задачу за раз в синхронном режиме. Главный поток также обрабатывает задачи рендеринга — рисование и верстку, — а также взаимодействие с пользователем. Таким образом, длительное выполнение задач JavaScript иногда приводит к тому, что браузер перестает реагировать на запросы. Именно поэтому веб-страницы могут «зависать» при выполнении тяжелых функций JavaScript, блокируя взаимодействие с пользователем.

Мы продемонстрируем, как заблокировать главный поток, смоделировав тяжелые вычисления с использованием алгоритма Фибоначчи, и решим проблему заблокированного главного потока, используя следующие подходы:

Алгоритм Фибоначчи

Для всех примеров в этой статье будем использовать простой и довольно распространенный алгоритм Фибоначчи с временной сложностью 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 секунды.

Инструменты оценки производительности показывают, что основной поток блокируется в течение 2 секунд

Многопоточность (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.

Инструменты оценки производительности показывают, что AssemblyScript на 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.

Инструменты оценки производительности показывают, что Rust на 66 % быстрее JavaScript

TL;DR: основные тезисы

  • Тяжелые вычисления блокируют основной поток и останавливают все виды анимации.
  • Тяжелые вычисления можно переложить на веб-воркер.
  • Скорость тяжелых вычислений можно оптимизировать, переписав логику на WebAssembly. Используя алгоритм Фибоначчи в качестве примера, мы получили следующие результаты:

— JavaScript: 2 с;

— WebAssembly — AssemblyScript: 953 мс (на 53 % быстрее, чем с JavaScript);

— WebAssembly — Rust: 684 мс (на 66 % быстрее, чем с JavaScript).

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Titus Efferian: Rust vs JavaScript: Achieving 66% Faster Performance with WebAssembly

Предыдущая статьяПроблема IDOR: несанкционированный отзыв сессии пользователя