-
Notifications
You must be signed in to change notification settings - Fork 1k
[lit-html] Use a double-keyed LRU cache for server rendered static html #5118
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
base: main
Are you sure you want to change the base?
Changes from all commits
4fd1dc2
9ed920c
7f443ef
a8e386d
cea99c6
81dd1be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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; | ||
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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. */ | ||||||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||
| * Wraps a lit-html template tag (`html` or `svg`) to add static value support. | ||||||||||||||||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code style nitpick; Feel free to ignore.
Suggested change
|
||||||||||||||||||||||||||||
| 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) | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
There was a problem hiding this comment.
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