Производительность web-приложений и работа с данными в браузере, WebSockets, HTTP/2

Технопарк, весна, 2019 г.

Производительность web-приложений
Слайды доступны по ссылке
frontend-park-mailru.firebaseapp.com

Из чего складывается производительность WEB'а?

Производительность WEB'а

Что происходит во время
HTTP-запроса?

Что происходит во время
HTTP-запроса?

HTTP-latency

HTTP-latency

Как оптимизировать?

prefetch, preconnect, preload

		<!--  резолвит DNS заранее -->
		<meta http-equiv="x-dns-prefetch-control" content="on">
		<link rel="dns-prefetch" href="//api.myawesomegame.io">
		<!-- dns-prefetch на стероидах, также инициирует коннект -->
		<link rel="preconnect" href="//api.myawesomegame.io">
		 
		<!-- загружает ресурс с низким приоритетом и кладет в кеш -->
		<link rel="prefetch" href="https://myawesomegame.io/img/sprite.jpg">
		<!-- загружает ресурс с высоким приоритетом и кладет в кеш -->
		<link rel="subresource" href="https://myawesomegame.io/img/sprite.jpg">
		 
		<!-- загружает страницу со всем содержимым в фоне, строит DOM -->
		<link rel="prerender" href="https://myawesomegame.io/about.html">
	

Предзагрузка картинок через JS

			const img = new Image();
			img.src = 'https://myawesomegame.io/img/sprite.jpg';
		

PerformanceTiming

		window.performance.timing
	
содержит все события, происходившие с загрузкой страницы

Performance Entries

		  // Покажет всю сетевую активность
		window.performance.getEntries()
		  // Отобразит все загружаемые ресурсы
		performance.getEntriesByType('resource')
	

DOMContentLoaded и load

DOMContentLoaded возникает на построение DOM-дерева
			document.addEventListener("DOMContentLoaded", () => {
			  // DOM дерево построено, но загружены не все ресурсы
			});
		
DOMContentLoaded зависит от скриптов на странице

load возникнет, когда все ресурсы (стили, шрифты, картинки) будут загружены
			document.addEventListener("load", () => {
			  // Все ресурсы загружены
			});
		

Уменьшаем время загрузки данных

Минификация статики (стили, скрипты, картинки)

jquery.js→ 278KB
jquery.min.js → 101KB

Подробнее про минификаторы JavaScript
https://learn.javascript.ru/minification
Видео про css минификатор
https://www.youtube.com/watch?v=8o3gKKD_J4A

Уменьшаем время загрузки данных

Сжатие ответа

		 // В запросе
		Accept-Encoding: gzip, deflate, br
		 // В ответе
		Content-Encoding: gzip
	
Плюсы: Минусы:

Zopfli и Brotli

Zopfli
Медленно сжимает, дает хороший результат, совместим с gzip
		 # Заранее сжимаем статику
		./zopfli -c app.js > app.js.gz
	
Необходимо включать отдачу сжатой статики на сервере
		 # пример для nginx
		gzip_static on;
	
Brotli

Zopfli и Brotli

Brotli
Необходимо включать на сервере
		 # пример для nginx
		brotli on;
	
zopfli vs brotli

Поддержка Brotli

Пример

http://zip.jsdaddy.tech http://zip.jsdaddy.tech/brotli

Уменьшаем время загрузки данных

Форматы

WebP (4,9 KB) GIF (85,2 KB)
Для видео существует WebM
Наилучшее сжатие для шрифтов осуществляет Woff2

So sad...

Protobuf

user.proto
		syntax = "proto3";
		message User {
		  string name = 1;
		  int32 age = 2;
		}
	
Компилируем в js
		npm install -g protobufjs
		pbjs -t static-module -w commonjs -o user.js user.proto
	
Можно использовать webpack-loader 😏

Protobuf VS. JSON

HTTP weakness.
One request per connection

HTTP1.1 заголовок Connection: keep-alive
позволит переиспользовать соединение после запроса
При получении Connection: close браузер закроет соединение

HTTP weakness.
Max. 6 connections per domain

Как с этим жить:

Кэширование

Кэширование

Кэширование — повторное использование ресурсов для работы приложения. Использование кэшей позволяет уменьшить задержку и расходование сетевого трафика и тем самым уменьшить время, необходимое для отображения ресурса. HTTP-кэширование — кэширование ответов на HTTP-запросы

Кэширование

			// Запрос обыкновенный
			GET /script.js HTTP/1.1
			Host: example.com
			Accept: */*
			 
		

Кэширование

			// Ответ на запрос
			HTTP/1.1 200 OK
			Content-Type: application/javascript; charset=UTF-8
			Cache-Control: public, max-age=86400
			Last-Modified: Sat, 25 Mar 2017 12:00:00 GMT
			ETag: W/"7349b-15b075b6d60"
			 
		

Cache-Control

Возможные значения

Проверка "свежести"

			// Ответ на запрос
			HTTP/1.1 200 OK
			...
			Last-Modified: Sat, 25 Mar 2017 12:00:00 GMT
			...
			 
		

Проверка "свежести"

			GET /script.js HTTP/1.1
			Host: example.com
			If-Modified-Since: Sat, 25 Mar 2017 12:00:00 GMT
			 
		
			HTTP/1.1 304 Not Modified   // Контент не изменился
			HTTP/1.1 200 OK             // Новый контент
			 
		

Проверка валидности

			// Ответ на запрос
			HTTP/1.1 200 OK
			...
			ETag: W/"7349b-15b075b6d60"
			...
			 
		

Проверка валидности

			GET /script.js HTTP/1.1
			Host: example.com
			If-None-Match: W/"7349b-15b075b6d60"
			 
		
			HTTP/1.1 304 Not Modified   // Контент не изменился
			HTTP/1.1 200 OK             // Новый контент
			 
		

"Управление" кэшированием

Хранение данных на клиенте

Как сохранить что-то в браузере
пользователя?

Все данные, которые используются web-приложением, существуют только пока открыта вкладка браузера. Однако, существуют способы сохранить какие-то данные в браузере и воспользоваться ими потом:

Web Storage API

Web Storage API — механизм для сохранения key/value значений с возможностью программного управления данными. Предоставляет два host объекта в браузере пользователя с возможностью персистентного сохранения данных (до 10 MB на origin)

window.localStorage

		// элементами Storage являются строки
		localStorage[key];           /* String */
		localStorage[key] = value;   /* String */
		 
		// работа с объектами Storage синхронная
		localStorage.length
		localStorage.key(i)          /* String */
		localStorage.getItem(key)    /* String */
		localStorage.setItem(key, value)  // может сгенерировать exception,
		                                  // если нет места
		localStorage.removeItem(key)
		localStorage.clear()
		 
	

JSON

Можно написать обёртку, которая позволит сохранять в Storage простые объекты:

		function setJSON(key, value) {
		    localStorage[key] = JSON.stringify(value);
		}
		function getJSON(key) {
		    const value = localStorage[key];
		    return value ? JSON.parse(value) : null;
		}
		 
	

Событие storage

Событие storage происходит при любых изменениях в Storage в других вкладках с того же origin. То есть это событие позволяет общаться между вкладками

		// обработчик добавляется на объект window
		window.addEventListener('storage', function (e) {
		    /* e.key, e.newValue */
		    ...
		});
		 
	

WebSQL — подробнее

WebSQL — полноценная SQL база данных, которая позволяет персистентно хранить данные в браузере пользователя и работать с ними посредством SQL-запросов. Максимальный размер сохраняемых данных — 5 MB. Поддержка на caniuse.

WebSQL пример

		// создаём объект базы данных (доступно и в воркерах!)
		const db = openDatabase('forum', 'v1.0.0', 'Forum', 100000);
		 
		// создаём транзакцию
		db.transaction(function(tx) {
		    tx.executeSql(
		        'SELECT COUNT(*) FROM `forum`',
		        [],
		        function (result) { console.log(result) },
		        function (tx, error) { /* some error logic */ }
		    );
		});
		 
	

IndexedDB — подробнее

IndexedDB — низкоуровневое API для клиентского хранилища большого объема структурированных данных, включая файлы/blobs. Эти API используют индексы для обеспечения высоко-производительного поиска данных. Максимальный размер сохраняемых данных — 50 MB!!! Поддержка на caniuse.

IndexedDB пример

		// открываем базу данных Forum (доступно и в воркерах!)
		const request = window.indexedDB.open('Forum', 3);  // 3 - версия бд
		 
		// обработчик успешного открытия базы данных
		request.onsuccess = function(event) {
		    const db = event.target.result;
		    const store = db.createObjectStore('users', { keyPath: 'userId' });
		 
		    store.createIndex('age', 'age', { unique: false });
		    store.createIndex('email', 'email', { unique: true });
		 
		    store.add({ age: 21, email: 'a.ostapenko@corp.mail.ru' });
		};
		 
	

FileSystem API — подробнее

deprecated =(((

С помощью FileSystem API и File API веб приложение может создавать, читать, просматривать и записывать файлы находящиеся в области пользовательской «песочницы». Крутой туториал. Поддержка на caniuse.

Service Workers

Must see

https://www.youtube.com/watch?v=cmGr0RszHc8
Building offline-first Progressive Web Apps

Service Workers

Service Workers

Service Workers

Service Workers

Service Workers — продвинутая технология, которая позволяет получить полный контроль над жизненным циклом приложения. Сервис воркер — это воркер, который:

Подробнее — по ссылке на MDN

Service Workers используются

Service Workers — caniuse

Service Workers

Работает в специальном скоупе ServiceWorkerGlobalScope, который не имеет доступа к обычному скоупу с window
Имеет несколько событий, на которые можно навешивать обработчики:

		this.addEventListener('install', listener);  // SW зарегистрировали
		this.addEventListener('activate', listener); // SW запустили
		this.addEventListener('fetch', listener);    // SW перехватил запрос
		this.addEventListener('message', listener);  // SW получил сообщение
		this.addEventListener('push', listener);  // SW получил push
		 
	

Service Worker lifecycle

Установка Service Worker'а

		navigator.serviceWorker.register('/sw.js', { scope: '/' })
		    .then(function(registration) {
		        // Registration was successful
		        console.log('SW registration OK:', registration);
		    })
		    .catch(function(err) {
		        // registration failed :(
		        console.log('SW registration FAIL:', err);
		    });
		});
		 
	

Service Worker notes

Service Worker — это просто файл

			this.addEventListener('install', function (event) {
			    console.log('Service worker установлен')
			    event.waitUntil(
			        // находим Cache-объект с нашим именем
			        caches.open('MY_CACHE')
			            .then(function (cache) {
			                // загружаем в наш cache необходимые файлы
			                return cache.addAll(['/index.html']);
			            });
			    );
			});
			 
		

Практика с Service Workers

Ещё примеры использования Service Workers

Больше протоколов!

Протокол WebSocket

WebSocket

Протокол WebSocket — протокол полнодуплексной связи (может передавать и принимать одновременно) поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб-сервером в режиме реального времени. С помощью его API вы можете отправить сообщение на сервер и получить ответ без выполнения отдельного HTTP-запроса, причем этот процесс будет событийно-управляемым

Были созданы, чтобы обойти ограничение HTTP на формат запрос/ответ и дать возможность отправлять сообщения с сервера на клиент

Подробнее — по ссылке на learn.javascript.ru

Поддержка браузерами — caniuse

Преимущества WebSocket

Именно поэтому WebSocket'ы очень удобно использовать для написания:

Создание WebSocket

		const ws = new WebSocket('ws://example.com/ws');
		 
		// если страница загружена по https://
		const ws = new WebSocket('wss://example.com/ws');
		 
		// События WebSocket
		ws.addEventListener('open', listener);     // соединение установлено
		ws.addEventListener('message', listener);  // пришло новое сообщение
		ws.addEventListener('error', listener);    // ошибка
		ws.addEventListener('close', listener);    // сокет закрылся
		 
	

Работа с WebSocket

После создания объекта WebSocket необходимо дождаться, пока соединение не откроется и не установится:

		ws.onopen = function() {
		    console.log('Соединение установлено, можно отправлять сообщения!');
		 
		    // Отправка текста
		    ws.send('Hello!');
		    ws.send(JSON.stringify({ x: 100, y: 150 }));
		 
		    // Отправка бинарных данных (например файлы из формы)
		    ws.send(form.elements[0].file);
		};
		 
	

Событие error и close

		ws.onerror = function(error) {
		    // произошла ошибка в отправке/приёме данных или сетевая ошибка
		    console.log('Ошибка ' + error.message);
		};
		 
		ws.onclose = function(event) {
		    // 1000 - штатное закрытие сокета (коды WebSocket из 4х цифр)
		    // 1001 - удалённая сторона исчезла
		    // 1002 - ошибка протокола
		    // 1003 - неверный запрос
		    console.log('Код: ' + event.code);
		    console.log('Причина: ' + event.reason);
		};
		 
	

Событие message — обработка
сообщений с сервера

		ws.onmessage = function(event) {
		    const data = event.data;
		    const message = JSON.parse(data);
		 
		    console.log('Прислали сообщение: ' + message.text);
		 
		    // или, если есть глобальная шина событий
		    bus.emit(message.event, message.payload);
		};
		 
	

Как использовать WebSocket

  1. Договориться о своём "надпротоколе" обмена сообщениями между клиентом и сервером — зафиксировать форматы всех сообщений в приложении. Например:
		{
		    "action": "FIRE",
		    "payload": { "cell": "b4" }
		}
		 
		{
		    "action": "FIRE_RESULT",
		    "payload": { "state": "Убил" }
		}
		 
	

Как использовать WebSocket

  1. Написать обёртку вокруг WebSocket, которая будет внутри себя заниматься отправкой и приёмом сообщений, а наружу будет предоставлять удобный интерфейс:
		const webSocketService = new WebSocketService('/ws');
		 
		webSocketService.send('FIRE', { "cell": "b4" });
		webSocketService.subscribe('FIRE_RESULT', function (payload) {
		    const state = payload.state;
		    game.reRender(state);
		});
		 
	

HTTP/2

Original HTTP/0.9 implementation

Hypertext Transfer Protocol: HTTP/1.1

Основные недостатки HTTP/1.1

HTTP/1.1 был спроектирован для сетей с более низкими пропускными способностями (bandwidth) и более высокими задержками (latency), чем сейчас. Поэтому у него есть недостатки:

HTTP/2

HTTP/2 создавался с целью улучшить скорость работы web-приложений, за счёт уменьшения сетевых задержек и более удобного управления ресурсами в web. Основные особенности:

HTTP/2 требует наличие
поддержки на сервере и на клиенте

Сервера, которые поддерживают HTTP/2:

HTTP/2 требует наличие
поддержки на сервере и на клиенте

Поддержка браузерами — caniuse

Главные фичи HTTP/2

Streams, Messages, and Frames

Stream — двунаправленный поток байтов через установленное соединение, который может состоять из одного или более сообщений

Message — целостная последовательность фреймов, которая составляет полное логическое сообщение: запрос или ответ

Frame — минимальная единица коммуникации в HTTP/2. Каждый фрейм содержит заголовок фрейма, который идентифицирует, к какому сообщению внутри стрима относится это фрейм

Streams, Messages, and Frames

Streams, Messages, and Frames

Бинарный формат сообщений

Бинарный формат сообщений

Мультиплексирование запросов
и ответов

Мультиплексирование запросов
и ответов

Приоретизация стримов

Один коннекшн на домен

Управление потоком данных

404 Slide Not Found

История, больше касающаяся серверсайда/реверс-прокси и прочих сисадминских штучек. Используется для более тщательного контроля над пересылкой данных, буферизацией и всяким таким добром

Server Push

Сжатие заголовков

Итак... HTTP/2 очень хорош!

Полезные ссылки

Всем спасибо!