-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
No compression in SSR mode #13
Comments
I hadn't considered SSR mode into this integration, I should probably add a note to the README that this will only work with static exports. If you inspect the import { defineMiddleware } from "astro:middleware";
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
export const onRequest = defineMiddleware(async (context, next) => {
const response = await next();
const html = await response.text();
const headers = response.headers;
let res: string | Buffer;
if (response.headers.get("Content-Type") === "text/html") {
const reqEncodings =
context.request.headers
.get("accept-encoding")
?.split(",")
.map((e) => e.trim()) ?? [];
if (reqEncodings?.includes("br")) {
headers.set("Content-Encoding", "br");
res = brotliCompressSync(html);
} else if (reqEncodings?.includes("gzip")) {
headers.set("Content-Encoding", "gzip");
res = gzipSync(html);
} else if (reqEncodings?.includes("deflate")) {
headers.set("Content-Encoding", "deflate");
res = deflateSync(html);
} else {
res = html;
}
}
return new Response(res, {
status: response.status,
headers: headers,
});
}); Note that the above is terrible for performance at scale, this is just meant as an example. 😃 |
Thanks for the reply and the snippet. Yes adding a note about it is a good idea Bye |
This comment was marked as off-topic.
This comment was marked as off-topic.
I tired to take a step forward to make it a bit more "production-ready", still a lot of things missing :) biggest issue I have is that I don't know how to make streams in node work :D the goal here was to make compression to cause as little latency as possible, which means I would have to figure out how to do http streaming 🫠 import { createBrotliCompress, deflateSync, gzipSync } from 'node:zlib';
import { pipeline, Writable } from 'stream';
import { defineMiddleware } from 'astro:middleware';
function skipStaticRoutes(url: URL) {
// to fix this warning:
// [WARN] `Astro.request.headers` is unavailable in "static" output mode, and in prerendered pages within "hybrid" and "server" output modes. If you need access to request headers, make sure that `output` is configured as either `"server"` or `output: "hybrid"` in your config file, and that the page accessing the headers is rendered on-demand.
const path = url.pathname;
return path === '/favicon.ico';
}
const cacheControlNoTransformRegExp = /(?:^|,)\s*?no-transform\s*?(?:,|$)/;
function shouldTransform(headers: Headers) {
const cacheControl = headers.get('Cache-Control');
// Don't compress for Cache-Control: no-transform
// https://github.com/Alorel/shrink-ray/blob/9696b1c69dd7e255947e8ce2c83e9774b3762000/index.js#L354-L361
return !cacheControl || !cacheControlNoTransformRegExp.test(cacheControl);
}
function getAcceptEncoding(headers: Headers) {
const acceptEncoding = headers
.get('accept-encoding')
?.split(',')
.map((e) => e.trim());
if (!acceptEncoding) {
return;
}
return acceptEncoding.includes('br')
? 'br'
: acceptEncoding.includes('gzip')
? 'gzip'
: acceptEncoding.includes('deflate')
? 'deflate'
: undefined;
}
function bodyToReadableStream(body: Body['body']) {
const readableStream = body?.getReader();
if (!readableStream) {
return;
}
const bodyReadableStream = new ReadableStream({
start(controller) {
return pump();
async function pump(): Promise<void> {
return readableStream?.read().then(({ done, value }) => {
// When no more data needs to be consumed, close the stream
if (done) {
controller.close();
return;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
// console.log('body read', value.length);
return pump();
});
}
},
});
return bodyReadableStream;
}
function createStreams() {
let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
const readStream = new ReadableStream({
start(ctrl) {
controller = ctrl;
},
});
const writeStream = new Writable({
write(chunk: Buffer, _encoding, callback) {
// console.log('writeStream', chunk.length);
try {
controller?.enqueue(chunk);
} catch (e) {
console.error('writeStream error:', e);
}
callback();
},
final(callback) {
controller?.close();
callback();
},
});
return { readStream, writeStream };
}
export const compress = defineMiddleware(async (context, next) => {
if (skipStaticRoutes(context.url)) {
return next();
}
const reqEncoding = getAcceptEncoding(context.request.headers);
if (reqEncoding === undefined) {
return next(); // Skip compression if none was requested
}
if (!shouldTransform(context.request.headers)) {
return next();
}
const response = await next();
// TODO: look into npm:compressible
if (response.headers.get('Content-Type') !== 'text/html') {
return next(); // Skip compression if the response is not HTML
}
// TODO: skip compression if content-length is too small
// TODO: skip HEAD requests
const headers = response.headers;
headers.set('Vary', 'Accept-Encoding');
headers.delete('Content-Length');
headers.set('Content-Encoding', reqEncoding);
if (reqEncoding === 'br') {
// convert request body to readable stream
const bodyReadableStream = bodyToReadableStream(response.body);
if (!bodyReadableStream) {
return next();
}
const compression = createBrotliCompress({});
const { readStream, writeStream } = createStreams();
const timer = setInterval(() => {
compression.flush();
}, 100);
compression.on('finish', () => {
clearInterval(timer);
});
pipeline(bodyReadableStream, compression, writeStream, (err) => {
if (err) {
console.error('Brotli compression failed:', err);
}
});
return new Response(readStream, {
status: response.status,
headers: headers,
});
}
console.time('compress');
const sendResponse = (res: Buffer) =>
new Response(res, {
status: response.status,
headers: headers,
});
// fallback to sync version of compression
// https://github.com/sondr3/astro-compressor/issues/13#issuecomment-1739721634
const html = await response.text();
console.timeEnd('compress');
if (reqEncoding === 'gzip') {
return sendResponse(gzipSync(html));
} else if (reqEncoding === 'deflate') {
return sendResponse(deflateSync(html));
}
return reqEncoding satisfies never;
}); |
Hi,
It seems that this is not working in SSR mode :(
My config files :
I try with and without "fileExtensions" but this not working.
My astro file :
The error :
Maybe it was not suppose to work like this...
Hope some help
Thanks
The text was updated successfully, but these errors were encountered: