precompressed.ts (3668B)
1 // Copyright (c) the JPEG XL Project Authors. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 import type {Context} from 'netlify:edge'; 7 8 // This lambda is executed whenever request URL matches. 9 export default async (request: Request, context: Context) => { 10 // Measure time for debugging purpose. 11 let t0 = Date.now(); 12 // Get resource path (i.e. ignore query parameters). 13 let url = request.url.split('?')[0]; 14 // Pick request headers; fallback to empty string if header is not set. 15 let acceptEncodingHeader = request.headers.get('Accept-Encoding') || ''; 16 let acceptHeader = request.headers.get('Accept') || ''; 17 let etag = request.headers.get('If-None-Match') || ''; 18 // Roughly parse encodings list; this ignores "quality"; no modern browsers 19 // use it -> don't care. 20 let splitter = /[,;]/; 21 let supportedEncodings = 22 acceptEncodingHeader.split(splitter).map(v => v.trimStart()); 23 let supportsBr = supportedEncodings.includes('br'); 24 let supportedMedia = acceptHeader.split(splitter).map(v => v.trimStart()); 25 let supportsJxl = supportedMedia.includes('image/jxl'); 26 // Dump basic request info (we care about). 27 context.log( 28 'URL: ' + url + '; acceptEncodingHeader: ' + acceptEncodingHeader + 29 '; supportsBr: ' + supportsBr + '; supportsJxl: ' + supportsJxl + 30 '; etag: ' + etag); 31 32 // If browser does not support Brotli/Jxl - just process request normally. 33 34 if (!supportsBr && !supportsJxl) { 35 return; 36 } 37 38 // Jxl processing is higher priority, because images are (usually) transferred 39 // with 'identity' content encoding. 40 let isJxlWorkflow = supportsJxl; 41 let suffix = isJxlWorkflow ? '.jxl' : '.br'; 42 43 // Request pre-compressed resource (with a suffix). 44 let response = await context.rewrite(url + suffix); 45 context.log('Response status: ' + response.status); 46 // First latency checkpoint (as we synchronously wait for resource fetch). 47 let t1 = Date.now(); 48 // If pre-compressed resource does not exist - pass. 49 if (response.status == 404) { 50 return; 51 } 52 // Get resource ETag. 53 let responseEtag = response.headers.get('ETag') || ''; 54 context.log('Response etag: ' + responseEtag); 55 // We rely on platform to check ETag; add debugging info just in case. 56 if (etag.length >= 4 && responseEtag === etag) { 57 console.log('Match; status: ' + response.status); 58 } 59 // Status 200 is regular "OK" - fetch resource; in such a case we need to 60 // craft response with the response contents. 61 // Status 3xx likely means "use cache"; pass response as is. 62 // Status 4xx is unlikely (404 has been already processed). 63 // Status 5xx is server error - nothing we could do around it. 64 if (response.status != 200) return response; 65 // Second time consuming operation - wait for resource contents. 66 let data = await response.arrayBuffer(); 67 let fixedHeaders = new Headers(response.headers); 68 69 if (isJxlWorkflow) { 70 fixedHeaders.set('Content-Type', 'image/jxl'); 71 } else { // is Brotli workflow 72 // Set "Content-Type" based on resource suffix; 73 // otherwise browser will complain. 74 let contentEncoding = 'text/html; charset=UTF-8'; 75 if (url.endsWith('.js')) { 76 contentEncoding = 'application/javascript'; 77 } else if (url.endsWith('.wasm')) { 78 contentEncoding = 'application/wasm'; 79 } 80 fixedHeaders.set('Content-Type', contentEncoding); 81 // Inform browser that data stream is compressed. 82 fixedHeaders.set('Content-Encoding', 'br'); 83 } 84 let t2 = Date.now(); 85 console.log('Timing: ' + (t1 - t0) + ' ' + (t2 - t1)); 86 return new Response(data, {headers: fixedHeaders}); 87 };