Как улучшить отзывчивость интерфейса с помощью Web Workers
JavaScript является однопоточным, поэтому любой запущенный JavaScript также останавливает отклик веб-страниц. Во многих случаях это не проблема, потому что код выполняется достаточно быстро, поэтому любое заикание пользовательского интерфейса практически незаметно для пользователя.
Однако это может стать серьезной проблемой, если код требует больших вычислительных ресурсов или если оборудование пользователя недостаточно мощно.
Веб-воркеры
Один из способов смягчить проблему - не перекладывать слишком много работы на основной поток, переложив работу на фоновые потоки. Другие платформы, такие как Android и iOS, подчеркивают важность того, чтобы основной поток обрабатывал как можно меньше работы, не связанной с пользовательским интерфейсом.
API Web Workers является веб - эквивалентом Android и IOS фоновых потоков. Более 97% браузеров поддерживают воркеры.
Демо
Давайте создадим демо, чтобы продемонстрировать проблему и решение. Вы также можете просмотреть окончательный результат на GitHub. Начнем с index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Worker Demo</title>
<script src="./index.js" async></script>
</head>
<body>
<p>The current time is: <span id="time"></span></p>
</body>
</html>
Далее, мы добавим index.js
и будем постоянно обновлять время и отображать его следующим образом: 21:45:08.345
function getTime() {
const now = new Date();
return (
padTime(now.getHours()) +
":" +
padTime(now.getMinutes()) +
":" +
padTime(now.getSeconds()) +
"." +
now.getMilliseconds()
);
}
setInterval(function () {
document.getElementById("time").innerText = getTime();
}, 50);
Установив для интервала значение 50 миллисекунд, мы очень быстро увидим обновление времени.
Настройка сервера
Затем мы запустим проект Node.js с помощью npm init
или yarn init
и установим Parcel. Первая причина, по которой мы хотим использовать Parcel, заключается в том, что в Chrome рабочие процессы должны обслуживаться, а не загружаться из локального файла.
Поэтому, когда мы добавим воркера позже, мы не сможем просто открыть index.html
, если будем использовать Chrome. Вторая причина заключается в том, что в Parcel есть встроенная поддержка веб-воркеров, которая не требует настройки для нашей демонстрации. Другие пакеты, такие как Webpack, потребуют дополнительных настроек.
Предлагаю добавить команду запуска в package.json
:
{
"scripts": {
"start": "parcel serve index.html --open"
}
}
Это позволит вам запуская npm start
или yarn start
создавать файлы, запускать сервер, открывать страницу в браузере и автоматически обновлять страницу при изменении исходных файлов.
Image-q
Теперь давайте добавим что-нибудь, что требует больших вычислительных ресурсов.
Мы установим image-q, библиотеку квантования изображений, которую мы будем использовать для вычисления основных цветов данного изображения, создавая цветовую палитру из изображения.
Вот пример:
Обновим body
:
<body>
<div class="center">
<p>The current time is: <span id="time"></span></p>
<form id="image-url-form">
<label for="image-url">Direct image URL</label>
<input
type="url"
name="url"
value="https://upload.wikimedia.org/wikipedia/commons/1/1f/Grapsus_grapsus_Galapagos_Islands.jpg"
/>
<input type="submit" value="Generate Color Palette" />
<p id="error-message"></p>
</form>
</div>
<div id="loader-wrapper" class="center">
<div id="loader"></div>
</div>
<div id="colors-wrapper" class="center">
<div id="color-0" class="color"></div>
<div id="color-1" class="color"></div>
<div id="color-2" class="color"></div>
<div id="color-3" class="color"></div>
</div>
<a class="center" id="image-link" target="_blank">
<img id="image" crossorigin="anonymous" />
</a>
</body>
Итак, мы добавляем форму, которая принимает прямую ссылку на изображение. Затем у нас есть загрузчик для отображения анимации вращения во время обработки. Мы адаптируем этот CodePen для его реализации. У нас также есть четыре блока, которые мы будем использовать для отображения цветовой палитры. Наконец, мы отобразим само изображение.
Добавьте несколько встроенных стилей в head
. Это включает в себя CSS-анимацию для вращающегося спинера.
<style type="text/css">
.center {
display: block;
margin: 0 auto;
max-width: max-content;
}
form {
margin-top: 25px;
margin-bottom: 25px;
}
input[type="url"] {
display: block;
padding: 5px;
width: 320px;
}
form * {
margin-top: 5px;
}
#error-message {
display: none;
background-color: #f5e4e4;
color: #b22222;
border-radius: 5px;
margin-top: 10px;
padding: 10px;
}
.color {
width: 80px;
height: 80px;
display: inline-block;
}
img {
max-width: 90vw;
max-height: 500px;
margin-top: 25px;
}
#image-link {
display: none;
}
#loader-wrapper {
display: none;
}
#loader {
width: 50px;
height: 50px;
border: 3px solid #d3d3d3;
border-radius: 50%;
border-top-color: green;
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
#error-message {
display: none;
background-color: #f5e4e4;
color: #b22222;
border-radius: 5px;
margin-top: 10px;
padding: 10px;
}
</style>
Обновление index.js
:
import * as iq from "image-q";
// Previous code for updating the time
function setPalette(points) {
points.forEach(function (point, index) {
document.getElementById("color-" + index).style.backgroundColor =
"rgb(" + point.r + "," + point.g + "," + point.b + ")";
});
document.getElementById("loader-wrapper").style.display = "none";
document.getElementById("colors-wrapper").style.display = "block";
document.getElementById("image-link").style.display = "block";
}
function handleError(message) {
const errorMessage = document.getElementById("error-message");
errorMessage.innerText = message;
errorMessage.style.display = "block";
document.getElementById("loader-wrapper").style.display = "none";
document.getElementById("image-link").style.display = "none";
}
document
.getElementById("image-url-form")
.addEventListener("submit", function (event) {
event.preventDefault();
const url = event.target.elements.url.value;
const image = document.getElementById("image");
image.onload = function () {
document.getElementById("image-link").href = url;
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
const imageData = context.getImageData(
0,
0,
image.naturalWidth,
image.naturalHeight
);
const pointContainer = iq.utils.PointContainer.fromImageData(imageData);
const palette = iq.buildPaletteSync([pointContainer], { colors: 4 });
const points = palette._pointArray;
setPalette(points);
};
image.onerror = function () {
handleError("The image failed to load. Please double check the URL.");
};
document.getElementById("error-message").style.display = "none";
document.getElementById("loader-wrapper").style.display = "block";
document.getElementById("colors-wrapper").style.display = "none";
document.getElementById("image-link").style.display = "none";
image.src = url;
});
Функция setPalette
устанавливает цвет фона цвета у дива, чтобы отобразить палитру. У нас также есть функция handleError
на случай, если изображение не загружается.
Затем мы слушаем отправку форм. Каждый раз, когда мы получаем новое представление, мы устанавливаем функцию onload
элементу изображения для извлечения данных изображения в формате, который подходит для image-q
.
Итак, мы рисуем изображение на холсте, чтобы получить объект ImageData.
Мы передаем этот объект image-q
и вызываем iq.buildPaletteSync
, что требует больших вычислительных ресурсов. Он возвращает четыре цвета, к которым мы обращаемся с помощью setPalette
.
При необходимости мы также скрываем и показываем элементы.
Эта проблема
Попробуйте создать цветовую палитру image-q
. Обратите внимание, что во время обработки время перестает обновляться. Если вы попытаетесь щелкнуть ввод URL-адреса, пользовательский интерфейс также не ответит. Однако анимация вращения может работать. Объяснение состоит в том, что вместо этого CSS-анимация может обрабатываться отдельным потоком композитора.
В Firefox браузер в конечном итоге отображает предупреждение:
Если у вас быстрый компьютер, проблема может быть не такой очевидной, потому что ваш процессор может быстро выполнять работу. Чтобы смоделировать более медленное устройство, вы можете использовать Chrome, в котором есть настройки инструментов разработчика для регулирования ЦП.
Откройте вкладку производительности, а затем ее настройки, чтобы отобразить параметр:
Добавление воркера
Чтобы исправить не отвечающий пользовательский интерфейс, давайте воспользуемся воркером. Во-первых, мы добавим в форму флажок, чтобы указать, должен ли сайт использовать воркер или нет. Добавьте этот HTML-код перед отправкой.
<input type="checkbox" name="worker" />
<label for="worker"> Use worker</label>
<br />
Далее мы настроим воркер в index.js
. Несмотря на то, что браузеры широко поддерживают web worker, давайте добавим проверку обнаружения функций с помощью if (window.Worker)
.
let worker;
if (window.Worker) {
worker = new Worker("worker.js");
worker.onmessage = function (message) {
setPalette(message.data.points);
};
}
Метод onmessage - это способ получения данных от воркера.
Затем мы изменим обработчик изображения onload
, чтобы использовать воркер, когда флажок установлен.
// From before
const imageData = context.getImageData(
0,
0
image.naturalWidth,
image.naturalHeight
);
if (event.target.elements.worker.checked) {
if (worker) {
worker.postMessage({ imageData });
} else {
handleError("Your browser doesn't support Web Workers.");
}
return;
}
Метод воркера postMessage - это то, как мы отправляем данные в worker.
Наконец, нам нужно создать сам воркер в worker.js
.
import * as iq from "image-q";
onmessage = function (e) {
const pointContainer = iq.utils.PointContainer.fromImageData(
e.data.imageData
);
const palette = iq.buildPaletteSync([pointContainer], { colors: 4 });
postMessage({ points: palette._pointArray });
};
Обратите внимание, что мы все еще используем onmessage
и postMessage
, но теперь onmessage
получает сообщение от index.js
, и postMessage
отправляет сообщение в index.js
.
Попробуйте создать палитру с помощью воркера, и вы увидите, что время постоянно обновляется во время обработки. Форма также остается интерактивной вместо зависания.
Вывод
Веб-воркеры - это эффективный способ сделать веб-сайты более отзывчивыми, особенно когда веб-сайт больше похож на приложение, а не на отображение в основном статических данных. Как мы видели, настройка воркера также может быть довольно простой, поэтому определение кода, интенсивно использующего процессор, и его перенос в воркер может быть легкой победой.
У воркеров есть ограничения, главное из которых - у них нет доступа к DOM
. Общий настрой должен заключаться в том, чтобы попытаться позволить основному потоку максимально сосредоточиться на пользовательском интерфейсе, включая обновление DOM, при одновременном переносе дорогостоящей работы в воркеры. Делая это, когда это имеет смысл, вы можете дать своим пользователям интерфейс, который не зависает и им всегда приятно пользоваться.