Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 9ef7463

Browse filesBrowse files
committed
feat(isr): add compression
1 parent 9ae21ca commit 9ef7463
Copy full SHA for 9ef7463

File tree

10 files changed

+159
-42
lines changed
Filter options

10 files changed

+159
-42
lines changed

‎apps/ssr-isr/server.ts

Copy file name to clipboardExpand all lines: apps/ssr-isr/server.ts
+10-2Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { CommonEngine } from '@angular/ssr';
22
import { ModifyHtmlCallbackFn } from '@rx-angular/isr/models';
3-
import { ISRHandler } from '@rx-angular/isr/server';
3+
import {
4+
compressHtml,
5+
CompressStaticFilter,
6+
ISRHandler,
7+
} from '@rx-angular/isr/server';
8+
import compression from 'compression';
49
import express, { Request } from 'express';
510
import { dirname, join, resolve } from 'node:path';
611
import { fileURLToPath } from 'node:url';
@@ -34,9 +39,12 @@ export function app(): express.Express {
3439
backgroundRevalidation: true, // will revalidate in background and serve the cache page first
3540
nonBlockingRender: true, // will serve page first and store in cache in background
3641
modifyGeneratedHtml: customModifyGeneratedHtml,
42+
compressHtml: compressHtml, // compress the html before storing in cache
43+
// cacheHtmlCompressionMethod: 'gzip', // compression method for cache
3744
// cache: fsCacheHandler,
3845
});
39-
46+
// compress js|css files
47+
server.use(compression({ filter: CompressStaticFilter }));
4048
server.use(express.json());
4149

4250
server.post(

‎libs/isr/models/src/index.ts

Copy file name to clipboardExpand all lines: libs/isr/models/src/index.ts
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {
77
VariantRebuildItem,
88
} from './cache-handler';
99
export {
10+
CompressHtmlFn,
1011
InvalidateConfig,
1112
ISRHandlerConfig,
1213
ModifyHtmlCallbackFn,

‎libs/isr/models/src/isr-handler-config.ts

Copy file name to clipboardExpand all lines: libs/isr/models/src/isr-handler-config.ts
+15Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ export interface ISRHandlerConfig {
124124
* If set to true, the server will provide the cached HTML as soon as possible and will revalidate the cache in the background.
125125
*/
126126
backgroundRevalidation?: boolean;
127+
128+
/**
129+
* compression callback to compress the html before storing it in the cache.
130+
* If not provided, the html will not be compressed.
131+
* If provided, the html will be compressed before storing it as base64 in the cache,
132+
* also this will disable the modifyCachedHtml callback, because html is compressed and can't be modified.
133+
*/
134+
compressHtml?: CompressHtmlFn;
135+
136+
/**
137+
* Cached Html compression method, it will use gzip by default if not provided.
138+
*/
139+
htmlCompressionMethod?: string;
127140
}
128141

129142
export interface ServeFromCacheConfig {
@@ -181,3 +194,5 @@ export interface RenderConfig {
181194
export interface RouteISRConfig {
182195
revalidate?: number | null;
183196
}
197+
198+
export type CompressHtmlFn = (html: string) => Promise<Buffer>;

‎libs/isr/server/src/cache-generation.ts

Copy file name to clipboardExpand all lines: libs/isr/server/src/cache-generation.ts
+10-8Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { Request, Response } from 'express';
44
import { ISRLogger } from './isr-logger';
55
import { defaultModifyGeneratedHtml } from './modify-generated-html';
66
import { getCacheKey, getVariant } from './utils/cache-utils';
7+
import { bufferToString } from './utils/compression-utils';
78
import { getRouteISRDataFromHTML } from './utils/get-isr-options';
89
import { renderUrl, RenderUrlConfig } from './utils/render-url';
910

1011
export interface IGeneratedResult {
11-
html?: string;
12+
html?: string | Buffer;
1213
errors?: string[];
1314
}
1415

@@ -22,7 +23,6 @@ export class CacheGeneration {
2223
public cache: CacheHandler,
2324
public logger: ISRLogger,
2425
) {}
25-
2626
async generate(
2727
req: Request,
2828
res: Response,
@@ -39,7 +39,6 @@ export class CacheGeneration {
3939

4040
return this.generateWithCacheKey(req, res, cacheKey, providers, mode);
4141
}
42-
4342
async generateWithCacheKey(
4443
req: Request,
4544
res: Response,
@@ -60,7 +59,6 @@ export class CacheGeneration {
6059

6160
this.urlsOnHold.push(cacheKey);
6261
}
63-
6462
const renderUrlConfig: RenderUrlConfig = {
6563
req,
6664
res,
@@ -72,17 +70,21 @@ export class CacheGeneration {
7270
browserDistFolder: this.isrConfig.browserDistFolder,
7371
inlineCriticalCss: this.isrConfig.inlineCriticalCss,
7472
};
75-
7673
try {
7774
const html = await renderUrl(renderUrlConfig);
7875
const { revalidate, errors } = getRouteISRDataFromHTML(html);
7976

8077
// Apply the modify generation callback
8178
// If undefined, use the default modifyGeneratedHtml function
82-
const finalHtml = this.isrConfig.modifyGeneratedHtml
79+
let finalHtml: string | Buffer = this.isrConfig.modifyGeneratedHtml
8380
? this.isrConfig.modifyGeneratedHtml(req, html, revalidate)
8481
: defaultModifyGeneratedHtml(req, html, revalidate);
85-
82+
let cacheString: string = finalHtml;
83+
// Apply the compressHtml callback
84+
if (this.isrConfig.compressHtml) {
85+
finalHtml = await this.isrConfig.compressHtml(finalHtml);
86+
cacheString = bufferToString(finalHtml);
87+
}
8688
// if there are errors, don't add the page to cache
8789
if (errors?.length && this.isrConfig.skipCachingOnHttpError) {
8890
// remove url from urlsOnHold because we want to try to regenerate it again
@@ -106,7 +108,7 @@ export class CacheGeneration {
106108

107109
// add the regenerated page to cache
108110
const addToCache = () => {
109-
return this.cache.add(cacheKey, finalHtml, {
111+
return this.cache.add(cacheKey, cacheString, {
110112
revalidate,
111113
buildId: this.isrConfig.buildId,
112114
});

‎libs/isr/server/src/compress-html.ts

Copy file name to clipboard
+14Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { CompressHtmlFn } from '@rx-angular/isr/models';
2+
import * as zlib from 'zlib';
3+
4+
export const compressHtml: CompressHtmlFn = (html: string): Promise<Buffer> => {
5+
return new Promise((resolve, reject) => {
6+
zlib.gzip(html, (err, buffer) => {
7+
if (err) {
8+
reject(err);
9+
} else {
10+
resolve(buffer);
11+
}
12+
});
13+
});
14+
};

‎libs/isr/server/src/index.ts

Copy file name to clipboardExpand all lines: libs/isr/server/src/index.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ export {
33
FileSystemCacheOptions,
44
} from './cache-handlers/filesystem-cache-handler';
55
export { InMemoryCacheHandler } from './cache-handlers/in-memory-cache-handler';
6+
export { compressHtml } from './compress-html';
67
export { IsrModule } from './isr.module';
78
export { ISRHandler } from './isr-handler';
89
export { IsrServerService } from './isr-server.service';
910
export { isrHttpInterceptors, provideISR } from './provide-isr';
11+
export { CompressStaticFilter } from './utils/compression-utils';

‎libs/isr/server/src/isr-handler.ts

Copy file name to clipboardExpand all lines: libs/isr/server/src/isr-handler.ts
+24-2Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CacheGeneration } from './cache-generation';
1212
import { InMemoryCacheHandler } from './cache-handlers/in-memory-cache-handler';
1313
import { ISRLogger } from './isr-logger';
1414
import { getCacheKey, getVariant } from './utils/cache-utils';
15+
import { setCompressHeader, stringToBuffer } from './utils/compression-utils';
1516

1617
export class ISRHandler {
1718
protected cache!: CacheHandler;
@@ -190,7 +191,11 @@ export class ISRHandler {
190191

191192
// Cache exists. Send it.
192193
this.logger.log(`Page was retrieved from cache: `, cacheKey);
193-
let finalHtml = html;
194+
let finalHtml: string | Buffer = html;
195+
196+
if (this.isrConfig.compressHtml) {
197+
finalHtml = stringToBuffer(finalHtml);
198+
}
194199

195200
// if the cache is expired, we will regenerate it
196201
if (cacheConfig.revalidate && cacheConfig.revalidate > 0) {
@@ -224,6 +229,20 @@ export class ISRHandler {
224229
}
225230
}
226231

232+
if (!this.isrConfig.compressHtml) {
233+
// Apply the callback if given
234+
// It doesn't work with compressed html
235+
if (config?.modifyCachedHtml) {
236+
const timeStart = performance.now();
237+
finalHtml = config.modifyCachedHtml(req, finalHtml as string);
238+
const totalTime = (performance.now() - timeStart).toFixed(2);
239+
finalHtml += `<!--\nℹ️ ISR: This cachedHtml has been modified with modifyCachedHtml()\n❗️
240+
This resulted into more ${totalTime}ms of processing time.\n-->`;
241+
}
242+
} else {
243+
setCompressHeader(res, this.isrConfig.htmlCompressionMethod);
244+
}
245+
227246
return res.send(finalHtml);
228247
} catch (error) {
229248
// Cache does not exist. Serve user using SSR
@@ -261,9 +280,12 @@ export class ISRHandler {
261280
config?.providers,
262281
'generate',
263282
);
264-
if (!result) {
283+
if (!result?.html) {
265284
throw new Error('Error while generating the page!');
266285
} else {
286+
if (this.isrConfig.compressHtml) {
287+
setCompressHeader(res, this.isrConfig.htmlCompressionMethod);
288+
}
267289
return res.send(result.html);
268290
}
269291
} catch (error) {
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import * as compression from 'compression';
2+
import * as express from 'express';
3+
4+
export function setCompressHeader(
5+
response: express.Response,
6+
method?: string,
7+
): void {
8+
response.setHeader('Content-Encoding', method || 'gzip');
9+
response.setHeader('Content-type', 'text/html; charset=utf-8');
10+
response.setHeader('Vary', 'Accept-Encoding');
11+
}
12+
13+
export function CompressStaticFilter(
14+
req: express.Request,
15+
res: express.Response,
16+
): boolean {
17+
const isStatic = new RegExp('.(?:js|css)$');
18+
if (!isStatic.test(req.url)) {
19+
// don't compress responses with this request header
20+
return false;
21+
}
22+
// fallback to standard filter function
23+
return compression.filter(req, res);
24+
}
25+
26+
export const bufferToString = (buffer: Buffer): string => {
27+
return buffer.toString('base64');
28+
};
29+
30+
export const stringToBuffer = (str: string): Buffer => {
31+
return Buffer.from(str, 'base64');
32+
};

‎package.json

Copy file name to clipboardExpand all lines: package.json
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@angular/ssr": "18.0.2",
5555
"@typescript-eslint/utils": "7.4.0",
5656
"bootstrap": "^5.2.3",
57+
"compression": "^1.7.4",
5758
"eslint-plugin-unused-imports": "^3.1.0",
5859
"ngx-skeleton-loader": "^7.0.0",
5960
"normalize-css": "^2.3.1",
@@ -91,7 +92,8 @@
9192
"@swc-node/register": "1.8.0",
9293
"@swc/core": "~1.3.85",
9394
"@types/benchmark": "^2.1.0",
94-
"@types/express": "4.17.14",
95+
"@types/compression": "^1.7.2",
96+
"@types/express": "4.17.21",
9597
"@types/jest": "^29.4.0",
9698
"@types/klaw-sync": "^6.0.0",
9799
"@types/lodash": "^4.14.196",

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.