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() {