Skip to content

Commit 316805b

Browse files
wycatschadhietala
authored andcommitted
Rehydration
This commit introduces a "rehydration" mode to the Glimmer VM. -- What is Rehydration? The rehydration feature means that the Glimmer VM can take a DOM tree created using Server Side Rendering (SSR) and use it as the starting point for the append pass. Rehydration is optimized for server-rendered DOMs that will not need many changes in the client, but it is also capable of making targeted repairs to the DOM: - if a single text node's value changes between the server and the client, the text node itself is repaired. - if the contents of a `{{{}}}` (or SafeString) change, only the bounds of that fragment of HTML are blown away and recreated. - if a mismatch is found, only the remainder of the current element is cleared. What this means in practice is that rehydration repairs problems in a relatively targeted way, and doesn't blows away more content than the contents of the current element when a mismatch occurs. Near-term work will narrow the scope of repairs further, so that mismatches inside of blocks only clear out the remainder of the content of the block. -- What Changes Were Needed? Previously, code in the append pass was permitted to make DOM changes at any point, so long as they made the changes through the stateless "DOM Helper" abstraction, which ensured that code didn't inadvertantly rely upon environment-specific behaviors. This includes both browser quirks (including the most recent version of Safari), and restricting Glimmer to the subset of DOM used in `SimpleDOM`, the library that is responsible for emulating the DOM in our current Server Side Rendering implementation. Rehydration works by replacing attempts to create DOM nodes with a check for whether the node that Glimmer is trying to create is already present. It maintains a "cursor" against the unhydrated DOM, and when Glimmer attempts to create an element, it first checks to see whether the candidate node matches the node that Glimmer is trying to create. For example, if Glimmer wants to create a text node, the rehydrator checks to see if the node under the cursor is a text node. If it is a text node, it updates its contents if necessary. If not, it begins a coarser-grained repair (which, as described above, is never worse than clearing out the rest of the DOM for the current element). This requires that the core DOM operations not only go through a stateless abstraction (DOM Helper), but also have a choke-point through a stateful builder that can maintain the candidate cursor and initiate repairs. Most of this commit restructures code so that DOM operations during the append pass go throug the central ElementBuilder in all cases. Part of this process involved restructuring the code that manages dynamic content and dynamic attributes to simplify them and make them a better fit for ensuring that all DOM operations in the append pass go through the ElementBuilder. -- Serialize Mode This commit also introduces a dedicated "serialize" mode that server-side renderers should use to generate the DOM. The serialize mode is reponsible for inserting additional markers into the DOM to serialize boundaries that otherwise serialize incorrectly. For example, it inserts a comment node between two adjacent text nodes so that they remain separated when rehydrating. The serialize mode does not prescribe a DOM implementation; any DOM that is compatible with the well-defined SimpleDOM subset will work (including a real browser's DOM, JSDOM, or SimpleDOM); it just ensures that the created DOM will round-trip through a string of HTML. Importantly, the rehydrator mode assumes that the server- provided DOM was created in serialize mode. -- Future Work: Attributes and Properties Today, when you write something like `<div title={{value}}>`, Glimmer attempts to use DOM properties if possible. This works, more or less, because the DOM automatically reflects properties to attributes in most cases. This means that if you write `<img src={{url}} />` in a template, we will set the `src` property, which the DOM automatically reflects onto the `src` attribute. Using properties where possible as opposed to attributes in all cases addresses a number of use cases. For example, - `<select value={{someValue}}>` will select the correct `<option>` automatically - `<div onclick={{action foo}}>` "just works" to set event handlers. - `<textarea value={{someValue}}>` updates the inside of the textarea instead of requiring you to deal with the text node contents of the textarea. We intend to move to all-attributes-all-the-time in the future, but we need to address these use-cases. There are various approaches under consideration (including a few special-cases, element modifiers, and an alternate sigil), but they are out of scope for this PR. In order to address this gap for now, this PR works around the slight semantic inconsistency. When the rehydrator pushes an element, it snapshots the list of all attributes that are already present on the element as a list of candidates for attribute removal. When Glimmer attempts to set an attribute **or property** on the element, the rehydrator removes it from the list of candidates for attribute removal. When an element's attributes are "flushed" (a step in the Glimmer VM), the rehydrator removes any attributes that it didn't see during the append step. This should work in the vast majority of cases because: First, it doesn't stop properties from being set, so any case where properties were set before continue to be set now. Second, if an attribute was present and the same-named property was set in the client, the attribute will remain in the DOM. This can only go wrong if the application was relying on a property being set that **didn't** set the same-named attribute. This is very rare; one example might be if an app was relying on using `form.reset()` to reset a field back to `''`, relying on the fact that `<input value={{someValue}}>` doesn't **actually** set the value attribute. This is very unlikely because empirically very few people use `.reset()` in Ember or Glimmer, which is **why using a property instead of an attribute here worked in the first place**. In short, if someone is relying on the fact that an element's attribute actually sets a property also **relies on the fact that it doesn't set an attribute,** the current workaround will repair inconsistently. This should be very rare and something we can address next.
1 parent 933acfb commit 316805b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2290
-1720
lines changed

.vscode/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
"**/node_modules": true,
1212
"**/dist": true,
1313
"dist": true,
14-
"tmp": true
14+
"tmp/**": true
15+
},
16+
"files.watcherExclude": {
17+
"**/.git/objects/**": true,
18+
"**/.git/subtree-cache/**": true,
19+
"**/node_modules/**": true,
20+
"tmp": true,
21+
"dist": true
1522
},
1623
"files.trimTrailingWhitespace": true,
1724
"editor.renderWhitespace": "boundary",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@
5858
"tslint": "^4.0.2",
5959
"typescript": "^2.2.0"
6060
}
61-
}
61+
}

packages/@glimmer/interfaces/lib/dom/simple.d.ts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ export type Namespace =
1010
| "http://www.w3.org/2000/xmlns/";
1111

1212
export enum NodeType {
13-
Element,
14-
Attribute,
15-
Text,
16-
CdataSection,
17-
EntityReference,
18-
Entity,
19-
ProcessingInstruction,
20-
Comment,
21-
Document,
22-
DocumentType,
23-
DocumentFragment,
24-
Notation
13+
Element = 1,
14+
Attribute = 2,
15+
Text = 3,
16+
CdataSection = 4,
17+
EntityReference = 5,
18+
Entity = 6,
19+
ProcessingInstruction = 7,
20+
Comment = 8,
21+
Document = 9,
22+
DocumentType = 10,
23+
DocumentFragment = 11,
24+
Notation = 12
2525
}
2626

2727
// This is the subset of DOM used by the appending VM. It is
@@ -31,12 +31,18 @@ export interface Node {
3131
nextSibling: Option<Node>;
3232
previousSibling: Option<Node>;
3333
parentNode: Option<Node>;
34-
nodeType: NodeType | number;
34+
nodeType: NodeType;
3535
nodeValue: Option<string>;
3636
firstChild: Option<Node>;
37+
lastChild: Option<Node>;
38+
}
39+
40+
export interface DocumentFragment extends Node {
41+
nodeType: NodeType.DocumentFragment;
3742
}
3843

3944
export interface Document extends Node {
45+
nodeType: NodeType.Document;
4046
createElement(tag: string): Element;
4147
createElementNS(namespace: Namespace, tag: string): Element;
4248
createTextNode(text: string): Text;
@@ -47,15 +53,38 @@ export interface CharacterData extends Node {
4753
data: string;
4854
}
4955

50-
export interface Text extends CharacterData {}
56+
export interface TokenList {
57+
[index: number]: string;
58+
length: number;
59+
60+
add(s: string): void;
61+
remove(s: string): void;
62+
contains(s: string): boolean;
63+
}
64+
65+
export interface Text extends CharacterData {
66+
nodeType: NodeType.Text;
67+
}
68+
69+
export interface Comment extends CharacterData {
70+
nodeType: NodeType.Comment;
71+
}
5172

52-
export interface Comment extends CharacterData {}
73+
export interface Attribute {
74+
name: string;
75+
value: string;
76+
}
77+
78+
export interface Attributes {
79+
[index: number]: Attribute;
80+
length: number;
81+
}
5382

5483
export interface Element extends Node {
84+
nodeType: NodeType.Element;
5585
namespaceURI: Option<string>;
5686
tagName: string;
57-
firstChild: Option<Node>;
58-
lastChild: Option<Node>;
87+
attributes: Attributes;
5988
removeAttribute(name: string): void;
6089
removeAttributeNS(namespaceURI: string, name: string): void;
6190
setAttribute(name: string, value: string): void;

packages/@glimmer/node/lib/node-dom-helper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as SimpleDOM from 'simple-dom';
2-
import { DOMTreeConstruction, Bounds, Simple, ConcreteBounds } from '@glimmer/runtime';
2+
import { DOMTreeConstruction, Bounds, ConcreteBounds } from '@glimmer/runtime';
3+
import { Simple } from '@glimmer/interfaces';
34

45
export default class NodeDOMTreeConstruction extends DOMTreeConstruction {
56
protected document: SimpleDOM.Document;

packages/@glimmer/node/test/node-test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as SimpleDOM from 'simple-dom';
22
import { TestEnvironment, TestDynamicScope } from "@glimmer/test-helpers";
3-
import { Template, Simple } from '@glimmer/runtime';
3+
import { Template } from '@glimmer/runtime';
4+
import { Simple } from '@glimmer/interfaces';
45
import { precompile } from '@glimmer/compiler';
56
import { UpdatableReference } from '@glimmer/object-reference';
67
import { NodeDOMTreeConstruction } from '@glimmer/node';
@@ -38,7 +39,7 @@ function commonSetup() {
3839
function render<T>(template: Template<T>, self: any) {
3940
let result;
4041
env.begin();
41-
let templateIterator = template.render(new UpdatableReference(self), root, new TestDynamicScope());
42+
let templateIterator = template.render({ self: new UpdatableReference(self), parentNode: root, dynamicScope: new TestDynamicScope() });
4243

4344
do {
4445
result = templateIterator.next();

packages/@glimmer/runtime/index.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import './lib/bootstrap';
22

3-
export { default as templateFactory, TemplateFactory, Template } from './lib/template';
3+
export { default as templateFactory, TemplateFactory, Template, TemplateIterator, RenderOptions } from './lib/template';
44

55
export { NULL_REFERENCE, UNDEFINED_REFERENCE, PrimitiveReference, ConditionalReference } from './lib/references';
66

@@ -25,26 +25,11 @@ export {
2525
CompiledDynamicProgram
2626
} from './lib/compiled/blocks';
2727

28-
export {
29-
AttributeManager as IAttributeManager,
30-
AttributeManager,
31-
PropertyManager,
32-
INPUT_VALUE_PROPERTY_MANAGER,
33-
defaultManagers,
34-
defaultAttributeManagers,
35-
defaultPropertyManagers,
36-
readDOMAttr
37-
} from './lib/dom/attribute-managers';
38-
3928
export {
4029
Register,
4130
debugSlice
4231
} from './lib/opcodes';
4332

44-
export {
45-
normalizeTextValue
46-
} from './lib/compiled/opcodes/content';
47-
4833
export {
4934
setDebuggerCallback,
5035
resetDebuggerCallback,
@@ -72,6 +57,12 @@ export {
7257

7358
export { PublicVM as VM, UpdatingVM, RenderResult, IteratorResult } from './lib/vm';
7459

60+
export {
61+
SimpleDynamicAttribute,
62+
DynamicAttributeFactory,
63+
DynamicAttribute
64+
} from './lib/vm/attributes/dynamic';
65+
7566
export {
7667
IArguments as Arguments,
7768
ICapturedArguments as CapturedArguments,
@@ -81,7 +72,7 @@ export {
8172
ICapturedNamedArguments as CapturedNamedArguments,
8273
} from './lib/vm/arguments';
8374

84-
export { SafeString, isSafeString } from './lib/upsert';
75+
export { SafeString } from './lib/upsert';
8576

8677
export {
8778
Scope,
@@ -109,8 +100,7 @@ export {
109100
ModifierManager
110101
} from './lib/modifier/interfaces';
111102

112-
export { default as DOMChanges, DOMChanges as IDOMChanges, DOMTreeConstruction, isWhitespace, insertHTMLBefore } from './lib/dom/helper';
113-
import * as Simple from './lib/dom/interfaces';
114-
export { Simple };
115-
export { ElementStack, ElementOperations } from './lib/builder';
103+
export { default as DOMChanges, SVG_NAMESPACE, DOMChanges as IDOMChanges, DOMTreeConstruction, isWhitespace, insertHTMLBefore } from './lib/dom/helper';
104+
export { normalizeProperty } from './lib/dom/props';
105+
export { ElementBuilder, NewElementBuilder, ElementOperations } from './lib/vm/element-builder';
116106
export { default as Bounds, ConcreteBounds } from './lib/bounds';

packages/@glimmer/runtime/lib/bounds.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as Simple from './dom/interfaces';
1+
import { Simple } from '@glimmer/interfaces';
22
import { Option, Destroyable } from '@glimmer/util';
33

44
export interface Bounds {
@@ -12,6 +12,16 @@ export class Cursor {
1212
constructor(public element: Simple.Element, public nextSibling: Option<Simple.Node>) {}
1313
}
1414

15+
export function currentNode(cursor: Cursor): Option<Simple.Node> {
16+
let { element, nextSibling } = cursor;
17+
18+
if (nextSibling === null) {
19+
return element.lastChild;
20+
} else {
21+
return nextSibling.previousSibling;
22+
}
23+
}
24+
1525
export default Bounds;
1626

1727
export interface DestroyableBounds extends Bounds, Destroyable {}
@@ -46,11 +56,11 @@ export class SingleNodeBounds implements Bounds {
4656
lastNode() { return this.node; }
4757
}
4858

49-
export function bounds(parent: Simple.Element, first: Simple.Node, last: Simple.Node): Bounds {
59+
export function bounds(parent: Simple.Element, first: Simple.Node, last: Simple.Node): ConcreteBounds {
5060
return new ConcreteBounds(parent, first, last);
5161
}
5262

53-
export function single(parent: Simple.Element, node: Simple.Node): Bounds {
63+
export function single(parent: Simple.Element, node: Simple.Node): SingleNodeBounds {
5464
return new SingleNodeBounds(parent, node);
5565
}
5666

0 commit comments

Comments
 (0)