Skip to content

Commit 515cef3

Browse files
perf: improve benchmark rendering performance
1 parent fbb3571 commit 515cef3

5 files changed

Lines changed: 485 additions & 145 deletions

File tree

bench/js-framework-benchmark/lib.mjs

Lines changed: 108 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -115,87 +115,145 @@ function frameworkDir(keyed) {
115115
function createArrowMainSource(keyed, mode) {
116116
const title = keyed ? 'ArrowJS (keyed)' : 'ArrowJS (non-keyed)'
117117
const importPath = mode === 'local' ? './arrow.js' : '@arrow-js/core'
118-
const rows = `() => {
119-
const items = data.items
120-
const rows = new Array(items.length)
121-
for (let i = 0; i < items.length; i++) {
122-
rows[i] = getRowView(items[i])
123-
}
124-
return rows
125-
}`
118+
const rows = `() => (data.version, views)`
126119
return `import { reactive, html } from '${importPath}';
127120
let data = reactive({
128-
items: [],
129-
selected: undefined,
121+
version: 0,
130122
});
123+
let ids = [];
124+
let labels = [];
125+
let views = [];
131126
132127
let rowId = 1;
133-
const rowViews = new WeakMap();
128+
let selectedIndex = -1;
134129
const adjectives = ["pretty", "large", "big", "small", "tall", "short", "long", "handsome", "plain", "quaint", "clean", "elegant", "easy", "angry", "crazy", "helpful", "mushy", "odd", "unsightly", "adorable", "important", "inexpensive", "cheap", "expensive", "fancy"];
135130
const colours = ["red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange"];
136131
const nouns = ["table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", "pizza", "mouse", "keyboard"];
137-
const add = () => data.items.push(...buildData(1000)),
132+
const labelPool = [];
133+
for (let i = 0; i < adjectives.length; i++) {
134+
const prefix = adjectives[i] + " ";
135+
for (let j = 0; j < colours.length; j++) {
136+
const stem = prefix + colours[j] + " ";
137+
for (let k = 0; k < nouns.length; k++) {
138+
labelPool.push(stem + nouns[k]);
139+
}
140+
}
141+
}
142+
const labelPoolSize = labelPool.length;
143+
const add = () => {
144+
appendData(1000);
145+
data.version++;
146+
},
138147
clear = () => {
139-
data.items.length = 0;
140-
data.selected = undefined;
148+
selectedIndex = -1;
149+
ids.length = 0;
150+
labels.length = 0;
151+
views.length = 0;
152+
data.version++;
141153
},
142154
partialUpdate = () => {
143-
const items = data.items;
144-
for (let i = 0; i < items.length; i += 10) {
145-
items[i].label += ' !!!';
155+
for (let i = 0; i < ids.length; i += 10) {
156+
labels[i] += ' !!!';
157+
views[i] = createRowView(ids[i], labels[i]);
146158
}
159+
data.version++;
147160
},
148161
run = () => {
149-
data.items = buildData(1000);
150-
data.selected = undefined;
162+
selectedIndex = -1;
163+
buildData(1000, ids, labels, views);
164+
data.version++;
151165
},
152166
runLots = () => {
153-
data.items = buildData(10000);
154-
data.selected = undefined;
167+
selectedIndex = -1;
168+
buildData(10000, ids, labels, views);
169+
data.version++;
155170
},
156171
swapRows = () => {
157-
const items = data.items;
158-
if (items.length > 998) {
159-
const item = items[1];
160-
items[1] = items[998];
161-
items[998] = item;
172+
if (ids.length > 998) {
173+
const id = ids[1];
174+
ids[1] = ids[998];
175+
ids[998] = id;
176+
const label = labels[1];
177+
labels[1] = labels[998];
178+
labels[998] = label;
179+
const view = views[1];
180+
views[1] = views[998];
181+
views[998] = view;
182+
if (selectedIndex === 1) selectedIndex = 998;
183+
else if (selectedIndex === 998) selectedIndex = 1;
184+
data.version++;
162185
}
163186
};
164187
165-
function getRowView(row) {
166-
let view = rowViews.get(row);
167-
if (view) return view;
168-
const id = row.id;
169-
view = html\`<tr class="\${() => data.selected === id ? 'danger' : ''}" data-id="\${id}"><td class="col-md-1">\${id}</td><td class="col-md-4"><a data-action="select">\${() => row.label}</a></td><td class="col-md-1"><a data-action="remove"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td><td class="col-md-6"></td></tr>\`${keyed ? `.key(id)` : ''};
170-
rowViews.set(row, view);
171-
return view;
188+
function createRowView(id, label, selected) {
189+
return html\`<tr class="\${selected ? 'danger' : false}"><td class="col-md-1">\${id}</td><td class="col-md-4"><a>\${label}</a></td><td class="col-md-1"><a><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td><td class="col-md-6"></td></tr>\`${keyed ? `.key(id)` : ''};
172190
}
173191
174192
function handleRowClick(event) {
175-
const target = event.target instanceof Element
176-
? event.target.closest('[data-action]')
193+
const cell = event.target instanceof Element
194+
? event.target.closest('td')
177195
: null;
178-
if (!target) return;
179-
const action = target.getAttribute('data-action');
180-
const id = Number(target.closest('tr')?.getAttribute('data-id'));
181-
if (!id) return;
182-
if (action === 'select') {
183-
data.selected = id;
196+
if (!cell) return;
197+
const index = cell.cellIndex;
198+
if (index !== 1 && index !== 2) return;
199+
const row = cell.parentElement;
200+
if (!(row instanceof HTMLTableRowElement)) return;
201+
const idx = row.sectionRowIndex;
202+
if (idx < 0 || idx >= ids.length) return;
203+
if (index === 1) {
204+
if (selectedIndex === idx) return;
205+
const previousIndex = selectedIndex;
206+
selectedIndex = idx;
207+
if (previousIndex > -1) {
208+
views[previousIndex] = createRowView(ids[previousIndex], labels[previousIndex], false);
209+
}
210+
views[idx] = createRowView(ids[idx], labels[idx], true);
211+
data.version++;
184212
return;
185213
}
186-
if (action === 'remove') {
187-
const idx = data.items.findIndex((row) => row.id === id);
188-
if (idx > -1) data.items.splice(idx, 1);
214+
if (index === 2) {
215+
if (idx === selectedIndex) {
216+
selectedIndex = -1;
217+
} else if (selectedIndex > idx) selectedIndex--;
218+
ids.splice(idx, 1);
219+
labels.splice(idx, 1);
220+
views.splice(idx, 1);
221+
data.version++;
189222
}
190223
}
191224
192-
function _random(max) { return Math.round(Math.random() * 1000) % max; };
225+
function buildData(count, ids, labels, views, start = 0) {
226+
const pool = labelPool;
227+
const size = labelPoolSize;
228+
const createView = createRowView;
229+
const end = start + count;
230+
let nextId = rowId;
231+
ids.length = end;
232+
labels.length = end;
233+
views.length = end;
234+
for (var i = start; i < end; i++) {
235+
const id = nextId++;
236+
const label = pool[(Math.random() * size) | 0];
237+
ids[i] = id;
238+
labels[i] = label;
239+
views[i] = createView(id, label, false);
240+
}
241+
rowId = nextId;
242+
}
193243
194-
function buildData(count = 1000) {
195-
const data = new Array(count);
196-
for (var i = 0; i < count; i++)
197-
data[i] = { id: rowId++, label: adjectives[_random(adjectives.length)] + " " + colours[_random(colours.length)] + " " + nouns[_random(nouns.length)] };
198-
return data;
244+
function appendData(count) {
245+
const pool = labelPool;
246+
const size = labelPoolSize;
247+
const createView = createRowView;
248+
let nextId = rowId;
249+
for (let i = 0; i < count; i++) {
250+
const id = nextId++;
251+
const label = pool[(Math.random() * size) | 0];
252+
ids.push(id);
253+
labels.push(label);
254+
views.push(createView(id, label, false));
255+
}
256+
rowId = nextId;
199257
}
200258
html\`<div class="container">
201259
<div class="jumbotron">

packages/core/src/__tests__/html.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,42 @@ describe('html', () => {
824824
</ul>`)
825825
})
826826

827+
it('updates keyed templates when the same key changes template shape', async () => {
828+
const data = reactive({
829+
list: [
830+
{ id: 1, label: 'Alpha', active: false },
831+
{ id: 2, label: 'Beta', active: false },
832+
],
833+
})
834+
const parent = document.createElement('div')
835+
836+
html`<ul>
837+
${() =>
838+
data.list.map((item) =>
839+
(item.active
840+
? html`<li class="danger">${item.label}</li>`
841+
: html`<li>${item.label}</li>`).key(item.id)
842+
)}
843+
</ul>`(parent)
844+
845+
expect(parent.innerHTML).toBe(`<ul>
846+
<li>Alpha</li><li>Beta</li>
847+
</ul>`)
848+
849+
data.list[0].active = true
850+
await nextTick()
851+
expect(parent.innerHTML).toBe(`<ul>
852+
<li class="danger">Alpha</li><li>Beta</li>
853+
</ul>`)
854+
855+
data.list[0].active = false
856+
data.list[1].active = true
857+
await nextTick()
858+
expect(parent.innerHTML).toBe(`<ul>
859+
<li>Alpha</li><li class="danger">Beta</li>
860+
</ul>`)
861+
})
862+
827863
it('can render results of multiple data objects', async () => {
828864
const a = reactive({ price: 45 })
829865
const b = reactive({ quantity: 25 })

packages/core/src/expressions.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { ArrowExpression } from './html'
1+
import { setAttr } from './dom'
2+
import type { ArrowExpression } from './html'
23

34
export const expressionPool: Array<number | ArrowExpression | undefined> = []
4-
const expressionObservers: Array<CallableFunction | undefined> = []
5+
const expressionObservers: Array<CallableFunction | Text | Element | undefined> = []
6+
const expressionObserverAttrs: Array<string | undefined> = []
57
const freeExpressionPointers: number[][] = []
68
let cursor = 0
79

@@ -24,15 +26,22 @@ export function writeExpressions(
2426
const target = pointer + i
2527
if (Object.is(expressionPool[target], nextValue)) continue
2628
expressionPool[target] = nextValue
27-
expressionObservers[target]?.(nextValue)
29+
const observer = expressionObservers[target]
30+
if (!observer) continue
31+
const attr = expressionObserverAttrs[target]
32+
if (attr !== undefined) setAttr(observer as Element, attr, nextValue as string)
33+
else if (typeof observer === 'function') observer(nextValue)
34+
else (observer as Text).data = nextValue || nextValue === 0 ? (nextValue as string) : ''
2835
}
2936
}
3037

3138
export function onExpressionUpdate(
3239
pointer: number,
33-
observer?: CallableFunction
40+
observer?: CallableFunction | Text | Element,
41+
attrName?: string
3442
): void {
3543
expressionObservers[pointer] = observer
44+
expressionObserverAttrs[pointer] = attrName
3645
}
3746

3847
export function releaseExpressions(pointer: number): void {
@@ -41,6 +50,7 @@ export function releaseExpressions(pointer: number): void {
4150
for (let i = 0; i <= len; i++) {
4251
expressionPool[pointer + i] = undefined
4352
expressionObservers[pointer + i] = undefined
53+
expressionObserverAttrs[pointer + i] = undefined
4454
}
4555
;(freeExpressionPointers[len] ??= []).push(pointer)
4656
}

0 commit comments

Comments
 (0)