Skip to content

Commit 2f84e0a

Browse files
[0004] lit-labs/compiler (#21)
1 parent f46f942 commit 2f84e0a

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed

rfcs/0004-template-compiler.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
---
2+
Status: Accepted
3+
Champions: "@AndrewJakubowicz"
4+
PR: "https://github.com/lit/rfcs/pull/21"
5+
---
6+
7+
# @lit-labs/compiler
8+
9+
A new labs package for preparing Lit templates at build time.
10+
11+
## Objective
12+
13+
Provide an optional build time performance improvement for the first render of a lit template.
14+
15+
### Goals
16+
17+
- Provide a build time transform that replaces lit `html` tagged template expressions with compiled templates.
18+
- Compiled and uncompiled template results can be mixed and matched transparently, and should be semantically identical.
19+
- Provide a rollup plugin to compile templates.
20+
- Compiler can be run on JavaScript and TypeScript files.
21+
- Compilation preserves source-maps.
22+
23+
### Non-Goals
24+
25+
- The compiled template does not have to be syntactically identical to templates prepared at runtime. For example comment markers will not be the same as runtime preparation.
26+
- Other template transformations. This is only for performance.
27+
28+
## Motivation
29+
30+
`lit-html` has [three internal rendering phases](https://github.com/lit/lit/blob/main/dev-docs/design/how-lit-html-works.md#summary-of-lit-html-rendering-phases) which are all optimized to cache and reuse previous work. However, the very first `render()` must still execute all three phases.
31+
32+
The @lit-labs/compiler will provide a way to opt into precompiling the templates, moving the first internal rendering phase to build time.
33+
34+
By precompiling `html` tagged template literals, the runtime only needs to execute two internal rendering phases reducing the first render performance. As a nice benefit, any subsequent updates that encounter a new compiled `html` tagged template literal will also benefit from the performance improvement and can skip the prepare phase.
35+
36+
## Detailed Design
37+
38+
At a high level, the `@lit-labs/compiler` package provides a transformer that converts:
39+
40+
```js
41+
const hi = (name) => html`<h1>Hello ${name}!</h1>`;
42+
```
43+
44+
into:
45+
46+
```js
47+
const compiled_html = (s) => s;
48+
const lit_template_1 = {
49+
h: compiled_html`<h1>Hello <?></h1>`,
50+
parts: [{ type: 2, index: 1 }],
51+
};
52+
const hi = (name) => ({ _$litType$: lit_template_1, values: [name] });
53+
```
54+
55+
This transform converts the `` html`<h1>Hello ${name}!</h1>` `` `TemplateResult` into a `CompiledTemplateResult` which semantically behaves the same but with greater initial render performance. The update performance is unchanged.
56+
57+
### Transformer Design
58+
59+
The transformer should be completely syntactic. Initially only compiling any `html` template expressions if `html` was imported from `lit` or `lit-html` directly.
60+
61+
Each individual `TemplateResult` is split into two declarations, a `CompiledTemplate` and `CompiledTemplateResult`. The `CompiledTemplate` is hoisted to the module scope, and contains the prepared HTML and template parts for the original `TemplateResult`. The `CompiledTemplateResult` replaces the `TemplateResult` and binds the static `CompiledTemplate` with the dynamic `values` array.
62+
63+
If a part is used that requires a constructor, the compiler
64+
will also insert an import such as:
65+
66+
```js
67+
// ...
68+
import { _$LH as litHtmlPrivate_1 } from "lit-html/private-ssr-support.js";
69+
const {
70+
AttributePart: _$LH_AttributePart,
71+
PropertyPart: _$LH_PropertyPart,
72+
BooleanAttributePart: _$LH_BooleanAttributePart,
73+
EventPart: _$LH_EventPart,
74+
} = litHtmlPrivate_1;
75+
```
76+
77+
This transformer will be exported from `@lit-labs/compiler`, and can be used anywhere TypeScript transformers are accepted, which depends on build setup. Below is an example using [Rollup](https://rollupjs.org/) with [@rollup/plugin-typescript](https://www.npmjs.com/package/@rollup/plugin-typescript):
78+
79+
```js
80+
// File: rollup.config.js
81+
import typescript from "@rollup/plugin-typescript";
82+
import { compileLitTemplates } from "@lit-labs/compiler";
83+
84+
export default {
85+
// ...
86+
plugins: [
87+
typescript({
88+
transformers: {
89+
before: [compileLitTemplates()],
90+
},
91+
}),
92+
// other rollup plugins
93+
],
94+
};
95+
```
96+
97+
#### What templates will not be compiled
98+
99+
The initial version of the compiler will not handle compilation of all templates. In the future, handling of all cases would allow us to completely remove the lit-html prepare phase from runtime.
100+
101+
| Name | Example | Why |
102+
| ------------------------------------ | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103+
| Re-exported `html` tag | `import {html} from 'my-rexported-lit';` | Any templates using this `html` tag will not be compiled, as it isn't imported from `lit` or `lit-html`. We will investigate ways to improve template detection. |
104+
| `svg` tag | `` svg`<line />` `` | `svg` tags would require a change in the create phase of `lit-html`. |
105+
| Invalid dynamic element name | `` html `<${'A'}></${'A'}>` `` | It's invalid. |
106+
| Invalid octal literals | `` html`\2` `` | It's invalid. |
107+
| Raw text elements with inner binding | `<textarea>${'not ok'}</textarea>` | Raw text elements (`script`, `style`, `textarea`, or `title`) with bindings get prepared at runtime by creating adjacent text nodes and markers. The `CompiledTemplate` prepared HTML cannot represent this so the `TemplateParts` nodeIndex cannot be correctly represented. |
108+
| `<template>` with internal binding | `` html `<template>${'not ok'}</template>` `` | Expressions are not supported inside `<template>`. [See Invalid Locations docs](https://lit.dev/msg/expression-in-template). |
109+
110+
### Rollup plugin
111+
112+
An additional `@lit-labs/compiler-rollup` package will be usable as a rollup plugin that operates on TypeScript and JavaScript. An example usage looks like:
113+
114+
```js
115+
// rollup.config.js (file truncated)
116+
import minifyHTML from "rollup-plugin-minify-html-literals";
117+
import litTemplateCompiler from "@lit-labs/compiler-rollup";
118+
119+
// ...
120+
plugins: [
121+
sourcemaps(),
122+
// optional: TypeScript compilation
123+
nodeResolve(),
124+
minifyHTML(),
125+
litTemplateCompiler(/** options **/), // <- @lit-labs/compiler-rollup plugin
126+
terser(),
127+
];
128+
// ...
129+
```
130+
131+
The compiler plugin should be ordered after all other plugins that operate on
132+
`html` tags, (such as `minifyHTML` and `@lit/localize`), but before any manglers
133+
or source code minifiers. The placement is required because once the compiler transforms
134+
the templates into compiled templates, `minifyHTML` will no longer work – as it only minifies
135+
uncompiled templates. Source code minifiers such as `terser` may rename the `html` tag
136+
function preventing the locating of the templates to compile, so must come after.
137+
138+
By operating on Javascript, this package should be less prone to breakages due to TypeScript version changes while still allowing the compiler to work in both JavaScript and TypeScript projects.
139+
140+
The following options can be passed into `litTemplateCompiler`:
141+
142+
| Option | Description |
143+
| --------- | ------------------------------------------------------------------------------- |
144+
| `include` | Input file glob patterns, used to include files to compile. |
145+
| `exclude` | File glob patterns to exclude from compilation. Take precedence over `include`. |
146+
147+
## Implementation Considerations
148+
149+
### Implementation Plan
150+
151+
Create a new `@lit-labs/compiler` package which exports a [TypeScript transformer](https://github.com/itsdouges/typescript-transformer-handbook#the-basics), and `@lit-labs/compiler-rollup` which vends a rollup plugin. No major changes in Lit core are required.
152+
153+
### Backward Compatibility
154+
155+
lit-html supports compiled templates on and above version 2.7.5.
156+
157+
### Testing Plan
158+
159+
Unit tests are sufficient for testing that the transform is applied correctly. An additional test target will be added to lit-html to compile the test suite using the transformer ensuring that all lit-html tests behave the same with a `CompiledTemplate`.
160+
161+
### Performance and Code Size Impact
162+
163+
This RFC has been prototyped: https://github.com/lit/lit/pull/3984 to gather benchmarks. Note that these are all subject to change based on the final implementation. These initial benchmarks support that compiling templates do result in a measurable performance improvement, with greater performance improvements occurring in template heavy contexts.
164+
165+
**Kitchen sink** average tachometer benchmark results:
166+
167+
- [**6.7% faster**]: `render-compiled`: 7.93ms - 8.10ms, `render-uncompiled`: 8.50ms - 8.69ms
168+
- [roughly same]: `update-compiled`: 17.92ms - 18.56ms, `update-uncompiled`: 18.08ms - 19.00ms
169+
- [roughly same]: `nop-update-compiled`: 5.53ms - 5.66ms, `nop-update-uncompiled`: 5.64ms - 5.80ms
170+
- [**9.3% larger gzipped file size**]: `index.js` uncompiled is 2216 bytes gzipped, or compiled it is 2443 bytes gzipped.
171+
172+
**Template Heavy** average tachometer benchmark results:
173+
174+
- [**46% faster**]: `render-compiled`: 9.51ms - 9.67ms, `render-uncompiled`: 17.66ms - 18.03ms
175+
- [**21% faster**]: `update-compiled`: 26.40 - 26.91ms, `update-uncompiled`: 33.62 - 34.51ms
176+
- [**5.6% larger gzipped file size**]: `template-heavy.js` uncompiled is 202,544 bytes gzipped, or compiled it is 214,495 bytes gzipped.
177+
178+
**repeat** average tachometer benchmark results:
179+
180+
- [**14% faster**]: `render-compiled`: 7.40ms - 7.62ms, `render-uncompiled`: 8.56ms - 8.94ms
181+
- [**8% faster**]: `update-compiled`: 189.57ms - 197.54ms, `update-uncompiled`: 206.51ms - 218.68ms
182+
- [**7.1% larger gzipped file size**]: `index.js` uncompiled is 3072 bytes gzipped, or compiled it is 3307 bytes gzipped.
183+
184+
### Interoperability
185+
186+
This labs package is interoperable as it can be safely run on any code, and will only optimize Lit templates.
187+
188+
### Security Impact
189+
190+
The prepared compiled template must be a tagged template literal to defend against JSON injection attacks. Otherwise the `@lit-labs/compiler` package is only a source code transform. Because a compiled template is processed in the same way that an uncompiled template is, we're just moving some of the work to build time. This should preserve the security invariants of the code being compiled.
191+
192+
### Documentation Plan
193+
194+
This package will initially be documented in its own README. If it stays on track to graduation, we should document this package under the _Tools and Workflows_ section on lit.dev. The package on GitHub will also include a TypeScript and JavaScript example of using `@lit-labs/compiler`.
195+
196+
## Downsides
197+
198+
The downside of this approach is that it requires the code to have a build step which can add complexity and build specific setups. There are also slight mismatches in the precompiled prepared HTML, such as lit markers which are not required with pre-compilation. The change in compiled comment markers broke one test in `lit_html` which was introspecting comment markers – something that should be uncommon in production use cases.
199+
200+
## Alternatives
201+
202+
We could not release this package, as the package will have initial implementation cost, and ongoing maintenance cost. The performance wins are large enough that it seems worth it.
203+
We expect the code to need only occasional maintenance from TypeScript updates, as Lit's template result API is very stable.

0 commit comments

Comments
 (0)