Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/five-jobs-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'lit-html': patch
---

Use a double-keyed LRU cache for server rendered static html
13 changes: 13 additions & 0 deletions packages/lit-html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,19 @@
},
"development": "./development/is-server.js",
"default": "./is-server.js"
},
"./lru-cache.js": {
"types": "./development/lru-cache.d.ts",
"browser": {
"development": "./development/lru-cache.js",
"default": "./lru-cache.js"
},
"node": {
"development": "./node/development/lru-cache.js",
"default": "./node/lru-cache.js"
},
"development": "./development/lru-cache.js",
"default": "./lru-cache.js"
}
},
"scripts": {
Expand Down
30 changes: 30 additions & 0 deletions packages/lit-html/src/lru-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export class LRUCache<K, V> extends Map<K, V> {
constructor(public readonly maxSize: number) {
if (maxSize <= 0) {
throw new Error('maxSize must be greater than 0');
}
super();
}

override set(key: K, value: V): this {
super.delete(key);
super.set(key, value);
if (this.size > this.maxSize) {
const keyToDelete = this.keys().next().value;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During the community call there was a question about performance of accessing this.keys().next().value.
From a brief, naive test case this seems acceptable.

Performance test case
const perfTest = (name, setup, test) => {
  console.log(`Starting performance test: ${name}`);
  const times = [];
  for (let i = 0; i < 100; i++) {
    const data = setup();
    const start = performance.now();
    test(data);
    times.push(performance.now() - start);
    if (i % 10 === 0) {
      console.log(`Completed ${i} runs`);
    }
  }
  const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
  console.log(`Average time over 100 runs: ${avgTime.toFixed(6)} ms`);
}

perfTest(
  'Small map key access',
  () => new Map().set(1, 1).set(2, 2),
  (data) => data.keys().next().value
);
perfTest(
  'Large map key access',
  () => [...Array(100000).keys()].map(() => crypto.randomUUID()).reduce((m, k) => m.set(k, k), new Map()),
  (data) => data.keys().next().value
);

if (keyToDelete !== undefined) {
this.delete(keyToDelete);
}
}
return this;
}

override get(key: K): V | undefined {
const value = super.get(key);
if (value !== undefined) {
// Deleting and setting the value again ensures the key is at the end of the LRU queue
this.delete(key);
this.set(key, value);
}
return value;
}
}
46 changes: 43 additions & 3 deletions packages/lit-html/src/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
// Any new exports need to be added to the export statement in
// `packages/lit/src/index.all.ts`.

import {isServer} from './is-server.js';
import {
html as coreHtml,
svg as coreSvg,
mathml as coreMathml,
TemplateResult,
} from './lit-html.js';
import {LRUCache} from './lru-cache.js';

export interface StaticValue {
/** The value to interpolate as-is into the template. */
Expand Down Expand Up @@ -106,7 +108,35 @@ export const literal = (
r: brand,
});

const stringsCache = new Map<string, TemplateStringsArray>();
// We double-key this cache so that usage of high cardinality static html
// templates doesn't cause a memory leak when running on the server. The
// browser environment is less prone to this issue because pages are
// frequently opened/closed/refreshed. We use an LRUCache in the server
// environment to limit the memory usage and a normal Map in the browser
// environment to avoid the payload size increase of including the LRUCache
// class in the bundle.
export interface Cache {
get(key: TemplateStringsArray): InnerCache | undefined;
set(key: TemplateStringsArray, value: InnerCache): Cache;
}

interface InnerCache {
get(key: string): TemplateStringsArray | undefined;
set(key: string, value: TemplateStringsArray): InnerCache;
}

let stringsCache: Cache;
if (isServer) {
stringsCache = new Map<
TemplateStringsArray,
LRUCache<string, TemplateStringsArray>
>();
} else {
stringsCache = new Map<
TemplateStringsArray,
Map<string, TemplateStringsArray>
>();
}
Comment on lines +128 to +139
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any difference here? Both paths initialize a Map and the type is given by Cache.
From my understanding this can simply be reduced to the following suggestion. Or am I missing something?

Suggested change
let stringsCache: Cache;
if (isServer) {
stringsCache = new Map<
TemplateStringsArray,
LRUCache<string, TemplateStringsArray>
>();
} else {
stringsCache = new Map<
TemplateStringsArray,
Map<string, TemplateStringsArray>
>();
}
const stringsCache: Cache = new Map();


/**
* Wraps a lit-html template tag (`html` or `svg`) to add static value support.
Expand Down Expand Up @@ -150,15 +180,25 @@ export const withStatic =
}

if (hasStatics) {
let innerCache = stringsCache.get(strings);
if (innerCache === undefined) {
if (isServer) {
innerCache = new LRUCache<string, TemplateStringsArray>(10);
} else {
innerCache = new Map<string, TemplateStringsArray>();
}
Comment on lines +185 to +189
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code style nitpick; Feel free to ignore.

Suggested change
if (isServer) {
innerCache = new LRUCache<string, TemplateStringsArray>(10);
} else {
innerCache = new Map<string, TemplateStringsArray>();
}
innerCache = isServer
? new LRUCache<string, TemplateStringsArray>(10)
: new Map<string, TemplateStringsArray>();

stringsCache.set(strings, innerCache);
}

const key = staticStrings.join('$$lit$$');
strings = stringsCache.get(key)!;
strings = innerCache.get(key)!;
if (strings === undefined) {
// Beware: in general this pattern is unsafe, and doing so may bypass
// lit's security checks and allow an attacker to execute arbitrary
// code and inject arbitrary content.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(staticStrings as any).raw = staticStrings;
stringsCache.set(
innerCache.set(
key,
(strings = staticStrings as unknown as TemplateStringsArray)
);
Expand Down
Loading