Реализация WebTransport для low-latency коммуникации на сайте
WebTransport — браузерный API поверх HTTP/3 (QUIC), доступный с Chrome 97, Edge 97, Firefox 114. В отличие от WebSocket, работает поверх UDP через QUIC, поддерживает несколько независимых потоков и датаграммы без head-of-line blocking. Для задач с требованием к задержке ниже 50 мс — это качественный шаг вперёд по сравнению с WebSocket.
Где WebTransport выигрывает у WebSocket
WebSocket — это один TCP-поток. Потеря пакета блокирует всю очередь (head-of-line blocking). При плохом соединении задержки растут непропорционально. QUIC решает это мультиплексированием: каждый поток независим, потеря пакета в одном не влияет на другие.
| Характеристика | WebSocket | WebTransport |
|---|---|---|
| Протокол | TCP | QUIC (UDP) |
| Мультиплексирование | Нет | Да (независимые потоки) |
| Датаграммы (ненадёжные) | Нет | Да |
| Head-of-line blocking | Есть | Нет (для разных потоков) |
| Поддержка браузеров | Все | Chrome 97+, Firefox 114+, Edge 97+ |
| 0-RTT reconnect | Нет | Да (QUIC session resumption) |
Требования к серверу
WebTransport требует HTTP/3. Варианты:
-
Go:
quic-go+webtransport-go -
Node.js:
@fails-components/webtransport(экспериментальный) -
Python:
aioquic - Cloudflare Workers: нативная поддержка WebTransport через Workers API
- nginx/Caddy: пока нет нативной поддержки WebTransport как прокси
Минимальный сервер на Go с webtransport-go:
package main
import (
"context"
"crypto/tls"
"log"
"net/http"
"github.com/quic-go/quic-go/http3"
"github.com/quic-go/webtransport-go"
)
func main() {
s := webtransport.Server{
H3: http3.Server{
Addr: ":4433",
TLSConfig: loadTLSConfig(), // TLS обязателен
},
}
http.HandleFunc("/wt", func(w http.ResponseWriter, r *http.Request) {
session, err := s.Upgrade(w, r)
if err != nil {
log.Printf("upgrade error: %v", err)
return
}
handleSession(session)
})
s.ListenAndServe()
}
func handleSession(session *webtransport.Session) {
ctx := context.Background()
for {
// Принимаем входящий двунаправленный поток
stream, err := session.AcceptStream(ctx)
if err != nil {
return
}
go handleStream(stream)
}
}
Клиентская часть: базовое подключение
const transport = new WebTransport('https://your-server:4433/wt');
// Ждём готовности
await transport.ready;
console.log('WebTransport connected');
transport.closed.then(() => console.log('Transport closed'));
// Обработка ошибок
transport.closed.catch(err => console.error('Transport error:', err));
Двунаправленные потоки (Bidirectional Streams)
Потоки — надёжные упорядоченные каналы, аналог нескольких независимых WebSocket в одном соединении:
// Клиент открывает поток
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
// Отправка
const encoder = new TextEncoder();
await writer.write(encoder.encode(JSON.stringify({ type: 'subscribe', channel: 'prices' })));
// Чтение ответов
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const message = JSON.parse(decoder.decode(value));
handleMessage(message);
}
Сервер может инициировать потоки в сторону клиента:
// Клиент принимает входящие потоки от сервера
const streamReader = transport.incomingBidirectionalStreams.getReader();
while (true) {
const { value: stream, done } = await streamReader.read();
if (done) break;
processServerStream(stream);
}
Датаграммы (Datagrams)
Ненадёжные, неупорядоченные UDP-like сообщения. Для позиций игроков, метрик, курсоров — где задержка важнее гарантии доставки:
// Отправка датаграммы
const datagramWriter = transport.datagrams.writable.getWriter();
const encoder = new TextEncoder();
function sendPosition(x, y) {
const data = encoder.encode(JSON.stringify({ x, y, ts: Date.now() }));
// Не ждём подтверждения, fire-and-forget
datagramWriter.write(data).catch(() => {}); // потеря пакета — норма
}
// Получение датаграмм
const datagramReader = transport.datagrams.readable.getReader();
const decoder = new TextDecoder();
(async () => {
while (true) {
const { value, done } = await datagramReader.read();
if (done) break;
const msg = JSON.parse(decoder.decode(value));
updateRemotePosition(msg);
}
})();
Однонаправленные потоки
Для потоковой отправки данных (стриминг событий, лог, бинарные данные):
// Клиент -> Сервер: однонаправленный поток
const sendStream = await transport.createUnidirectionalStream();
const writer = sendStream.getWriter();
await writer.write(encodeChunk(data));
await writer.close();
// Сервер -> Клиент: входящие однонаправленные потоки
const incomingReader = transport.incomingUnidirectionalStreams.getReader();
while (true) {
const { value: stream, done } = await incomingReader.read();
if (done) break;
const reader = stream.getReader();
// читаем данные из stream
}
Реальный кейс: торговый терминал
Биржевые котировки требуют минимальной задержки. Архитектура с WebTransport:
Биржевой фид (UDP) -> Go-сервер -> WebTransport -> Браузер
- Датаграммы для тиков цен (fire-and-forget, потеря 1–2% тиков допустима)
- Надёжный поток для ордеров и подтверждений
- Отдельный поток для подписок на инструменты
class MarketDataClient {
constructor(url) {
this.transport = null;
this.orderStream = null;
this.subscriptions = new Map();
}
async connect(url) {
this.transport = new WebTransport(url);
await this.transport.ready;
// Создаём поток для ордеров (надёжный)
this.orderStream = await this.transport.createBidirectionalStream();
// Слушаем датаграммы (тики)
this.listenTicks();
}
async listenTicks() {
const reader = this.transport.datagrams.readable.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const tick = decodeTick(value); // бинарный протобаф
this.subscriptions.get(tick.symbol)?.(tick);
}
}
async placeOrder(order) {
const writer = this.orderStream.writable.getWriter();
await writer.write(encodeOrder(order));
writer.releaseLock();
// Читаем подтверждение из того же потока
return readOrderConfirmation(this.orderStream.readable);
}
}
Проблема TLS-сертификата в разработке
WebTransport требует валидный TLS. В dev-среде — два варианта:
1. Chrome флаг для self-signed:
chrome://flags/#allow-insecure-localhost
2. Certificate pinning через serverCertificateHashes:
const transport = new WebTransport('https://localhost:4433/wt', {
serverCertificateHashes: [{
algorithm: 'sha-256',
value: hexToArrayBuffer('YOUR_CERT_SHA256_HASH'),
}],
});
Сертификат должен быть выдан максимум на 14 дней при использовании этого метода — генерируется скриптом при запуске dev-сервера.
Fallback стратегия
WebTransport не поддерживается Safari (по состоянию на начало 2026). Нужен graceful fallback:
async function createTransport(url) {
if ('WebTransport' in window) {
try {
const wt = new WebTransport(url.replace('wss://', 'https://'));
await wt.ready;
return new WebTransportAdapter(wt);
} catch (e) {
console.warn('WebTransport failed, falling back to WebSocket');
}
}
return new WebSocketAdapter(url);
}
Адаптер скрывает разницу за единым интерфейсом send(data) / on('message', cb).
Сроки
- Прототип с датаграммами и одним потоком — 2–3 дня
- Production-сервер на Go с TLS + комплексной обработкой потоков — 1 неделя
- Полноценный клиент с fallback на WebSocket, мониторингом задержек, reconnect — 2–3 недели
- Интеграция с существующим real-time приложением (замена WebSocket) — 3–5 дней при наличии адаптерного слоя







