perf(webclient): pipeline WebSocket upgrade request in polyfill
Bundle HTTP upgrade request with SDK_CONNECT message: - Build upgrade request during _connect() instead of after tunnel established - Include upgrade bytes in SDK_CONNECT message for backend to send immediately - Remove _sendWebSocketUpgrade() call from _handleConnectSuccess() This eliminates 1 RTT from WebSocket connection establishment: - Before: SDK_CONNECT -> wait -> SDK_CONNECT_SUCCESS -> send upgrade -> wait - After: SDK_CONNECT + upgrade -> tunnel established -> upgrade already sent Impact: Reduces overseas WebSocket latency from 500ms+ to 250ms+.
cognitive-glitch committed
Dec 9, 2025 at 14:12 UTC
f9cb70be2787b3175757c6757d914be9e05b220b
1 file changed
+42
-5
cmd/webclient/polyfill.js
+42
-5
@@ -188,6 +188,31 @@
188
});
189
}
190
191
+ // Build HTTP upgrade request bytes (used for pipelining)
192
+ _buildUpgradeRequest() {
193
+ // Parse URL to get path with query parameters
194
+ const path = (this._parsedUrl.pathname || "/") + (this._parsedUrl.search || "");
195
+ const host = this._parsedUrl.host;
196
+
197
+ // Build HTTP Upgrade request
198
+ let upgradeRequest = `GET ${path} HTTP/1.1\r\n`;
199
+ upgradeRequest += `Host: ${host}\r\n`;
200
+ upgradeRequest += `Upgrade: websocket\r\n`;
201
+ upgradeRequest += `Connection: Upgrade\r\n`;
202
+ upgradeRequest += `Sec-WebSocket-Key: ${this._wsKey}\r\n`;
203
+ upgradeRequest += `Sec-WebSocket-Version: 13\r\n`;
204
+
205
+ if (this._protocols) {
206
+ const protocolStr = Array.isArray(this._protocols)
207
+ ? this._protocols.join(', ')
208
+ : this._protocols;
209
+ upgradeRequest += `Sec-WebSocket-Protocol: ${protocolStr}\r\n`;
210
+ }
211
+
212
+ upgradeRequest += `\r\n`;
213
+ return upgradeRequest;
214
+ }
215
+
216
async _connect() {
217
debugLog("[WebSocket Polyfill] Connecting via Service Worker SDK to:", this._url);
218
try {
@@ -204,14 +229,27 @@
229
230
debugLog("[WebSocket Polyfill] Lease name:", leaseName);
231
232
+ // Build upgrade request for pipelining (reduces RTT from 2 to 1)
233
+ const upgradeRequest = this._buildUpgradeRequest();
234
+ debugLog("[WebSocket Polyfill] Pipelining upgrade request with connect");
235
+
236
+ // Set up for upgrade response before sending
237
+ this._waitingForUpgrade = true;
238
+ this._upgradeBuffer = new Uint8Array(0);
239
+
240
// Wait for Service Worker to be ready
241
await navigator.serviceWorker.ready;
242
210
- // Send connect message to Service Worker (note: connId not set yet, so we manually include clientId)
243
+ // Convert upgrade request to bytes for pipelining
244
+ const encoder = new TextEncoder();
245
+ const upgradeBytes = encoder.encode(upgradeRequest);
246
+
247
+ // Send connect message with bundled upgrade request (pipelining)
248
navigator.serviceWorker.controller.postMessage({
249
type: "SDK_CONNECT",
250
clientId: this._clientId,
251
leaseName: leaseName,
252
+ upgradeRequest: upgradeBytes, // Bundled for pipelining
253
});
254
255
} catch (error) {
@@ -223,10 +261,9 @@
261
_handleConnectSuccess(data) {
262
this._connId = data.connId;
263
226
- debugLog("[WebSocket Polyfill] E2EE connection established, sending WebSocket upgrade");
227
-
228
- // Send WebSocket HTTP Upgrade request
229
- this._sendWebSocketUpgrade();
264
+ // Upgrade request was already pipelined with SDK_CONNECT
265
+ // Just wait for upgrade response in _handleData
266
+ debugLog("[WebSocket Polyfill] E2EE tunnel established, upgrade request already sent (pipelined)");
267
}
268
269
_sendWebSocketUpgrade() {