Skip to content
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

* Added:
* Warn in the console when duplicate ids are detected during a morph, since they can cause subtle state loss (@botandrose) #142
* New off-by-default `skipUnchanged` option that skips morphing subtrees whose old and new content are already identical, for large speedups on mostly-unchanged pages (@myabc) #144

* Fixed:
* Fix TypeError when restoring focus to an element that doesn't support text selection (@emaia) #150
Expand Down
9 changes: 9 additions & 0 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ npm run perf [versus=morphdom] [benchmarks...]
### Arguments
* The optional `versus` argument can be used to compare with morphdom (the default), previous Idiomorph releases specified by the git release tag, e.g. `v0.3.0`, or a path to a local `.js` file that defines `Idiomorph`.
* The optional `benchmarks` argument can be used to run specific benchmarks, defaulting to all of them.
* The optional `--options='<json>'` argument passes a config object to `Idiomorph.morph` in both runs, e.g. `--options='{"skipUnchanged":true}'`. A previous version that does not know the option ignores it, which makes this the way to measure an opt-in option against the code it replaces. Pass it after `--` so npm does not swallow it.

Examples:
Running only the `table` and `checkboxes` benchmarks against morphdom:
Expand All @@ -36,6 +37,14 @@ cp src/idiomorph.js tmp/before.js # then edit src/idiomorph.js
npm run perf tmp/before.js
```

Measuring an opt-in option against the current code:
```bash
cp src/idiomorph.js tmp/before.js
npm run perf -- tmp/before.js --options='{"skipUnchanged":true}'
```

## Adding Benchmarks
You can add more benchmarks by creating new `benchmark-name.old.html` and `benchmark-name.new.html` files in the `perf/benchmarks` directory, containing the starting and final morph HTML respectively.

`deep-last-leaf` is generated by `node perf/generate-deep-last-leaf.js` and is the worst case among the committed benchmarks for the `skipUnchanged` option: every section changes, but only in its last and deepest text node, so every `isEqualNode` call walks a whole subtree before failing. Its equal filler siblings at each level are still individually skippable, so in practice it measures near-parity rather than a large regression.

13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Idiomorph supports the following options:
| `ignoreActive: false` | If `true`, idiomorph will skip the active element | `Idiomorph.morph(..., {ignoreActive:true})` |
| `ignoreActiveValue: false` | If `true`, idiomorph will not update the active element's value | `Idiomorph.morph(..., {ignoreActiveValue:true})` |
| `restoreFocus: true` | If `true`, idiomorph will attempt to restore any lost focus and selection state after the morph. | `Idiomorph.morph(..., {restoreFocus:true})` |
| `skipUnchanged: false` | If `true`, idiomorph will not descend into subtrees that are already identical. See the [skipping unchanged content](#skipping-unchanged-content) section | `Idiomorph.morph(..., {skipUnchanged:true})` |
| `head: {style: 'merge', ...}` | Allows you to control how the `head` tag is merged. See the [head](#the-head-tag) section for more details | `Idiomorph.morph(..., {head:{style:'merge'}})` |
| `callbacks: {...}` | Allows you to insert callbacks when events occur in the morph lifecycle. See the callback table below | `Idiomorph.morph(..., {callbacks:{beforeNodeAdded:function(node){...}})` |

Expand All @@ -107,6 +108,18 @@ of the algorithm.
| afterNodeRemoved(node) | Called after a node is removed from the DOM | none |
| beforeAttributeUpdated(attributeName, node, mutationType) | Called before an attribute on an element is updated or removed (`mutationType` is either "update" or "remove") | return false to not update or remove the attribute |

### Skipping unchanged content

Most real-world morphs change only a small part of a large page. With `skipUnchanged: true`, idiomorph compares each pair of old and new elements with [`isEqualNode`](https://developer.mozilla.org/en-US/docs/Web/API/Node/isEqualNode) and, when they are identical, leaves the whole subtree alone instead of walking into it. On pages that mostly stay the same this makes morphs many times faster. On pages where most of the content changes between morphs, though, the extra comparisons can cost slightly more than they save, so the option is best suited to mostly-unchanged pages.

Idiomorph's own morphing produces the same DOM with or without the option. What changes is what your callbacks see, and callbacks that rely on being called for every node can therefore behave differently:

* `beforeNodeMorphed` and `afterNodeMorphed` are still called for the root of an unchanged subtree, so you can still veto it, but they are **not** called for its descendants. `beforeAttributeUpdated` is never called inside it either, since nothing changes.
* Hidden state is respected: an `<input>`, `<textarea>` or `<option>` whose `value`, `checked` or `selected` property differs from its effective default (what parsing its markup would produce β€” an attribute-less checkbox legitimately has the value `on`, an untouched single-select has its first enabled option selected) is never skipped, nor are its ancestors, so it is synced exactly as without the option. `<template>` and `<head>` elements are never skipped either.
* If a `beforeNodeMorphed` callback changes such hidden state on the two nodes it was handed, that is honoured. Changes it makes to *descendants* of those nodes are not: an unchanged subtree may already have been skipped by the time they would be visited, so such side effects can leave the DOM different from a morph without the option.

The option is off by default in this release.

### The `head` tag

The head tag is treated specially by idiomorph because:
Expand Down
Loading