micra.js 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to Micra.js will be documented in this file. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.2.1] — 2026-05-28
8
+
9
+ ### Performance — batched first list render
10
+
11
+ - **First render of a keyed `data-each` list now inserts in a single DOM
12
+ operation.** `renderKeyed` previously appended each new row with an
13
+ individual `anchor.after(node)` call — N insertions for an N-row list. On the
14
+ initial render (no previous rows to diff against), all freshly-cloned rows are
15
+ now collected into one `DocumentFragment` and inserted with a single
16
+ `marker.after()`, skipping the LIS reorder pass entirely. The update, swap, and
17
+ reorder paths are unchanged.
18
+ - No public-API change. Bundle stays at **5.4 KB gzip**.
19
+
7
20
  ## [2.2.0] — 2026-05-27
8
21
 
9
22
  ### Performance — single-pass DOM scan
@@ -18,7 +18,7 @@ import type { StateRecord } from '../types';
18
18
  * const state = createReactiveState(raw, render)
19
19
  * state.count = 5 // triggers render() in next microtask
20
20
  */
21
- export declare function createReactiveState<S extends StateRecord>(obj: S, schedule: () => void): S;
21
+ export declare function createReactiveState<S extends StateRecord>(obj: S, schedule: () => void, onKey?: (key: string) => void): S;
22
22
  /**
23
23
  * Return a debounce function that defers `render` to the next microtask.
24
24
  * Multiple calls within the same tick collapse to a single render.
@@ -19,9 +19,10 @@ import type { InternalInstance, StateRecord } from '../types';
19
19
  * Process all `<template data-each>` elements found by the scanner.
20
20
  * Scoped itemState makes `item`, `index`, `$index` available in row expressions.
21
21
  *
22
- * @param templates - Pre-scanned list of <template data-each> elements
23
- * @param state - Expression state (proxy merging rawState + instance)
24
- * @param rawState - Raw (non-proxy) state — used for model binding
25
- * @param instance - Component instance (for event binding)
22
+ * @param templates - Pre-scanned list of <template data-each> elements
23
+ * @param state - Expression state (proxy merging rawState + instance)
24
+ * @param rawState - Raw (non-proxy) state — used for model binding
25
+ * @param instance - Component instance (for event binding)
26
+ * @param triggerKey - Which state key triggered this render (null = initial, 'MULTIPLE' = batch)
26
27
  */
27
- export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>): void;
28
+ export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>, triggerKey: string | null | 'MULTIPLE'): void;
package/dist/micra.cjs.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v2.2.0 — https://github.com/micra-js/micra — MIT */
1
+ /* Micra.js v2.2.1 — https://github.com/micra-js/micra — MIT */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -176,27 +176,32 @@ function safeStateHas(state, key) {
176
176
  return false;
177
177
  }
178
178
  function evalExpr(expr, state) {
179
- if (SIMPLE_PATH.test(expr)) {
180
- const parts = expr.split(".");
181
- if (!safeStateHas(state, parts[0])) return void 0;
182
- return parts.reduce(
179
+ let cached = exprCache.get(expr);
180
+ if (!cached) {
181
+ if (SIMPLE_PATH.test(expr)) {
182
+ cached = { kind: "path", parts: expr.split(".") };
183
+ } else {
184
+ try {
185
+ cached = {
186
+ kind: "fn",
187
+ fn: new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
188
+ };
189
+ } catch {
190
+ warn(`invalid expression "${expr}"`);
191
+ cached = { kind: "fn", fn: () => void 0 };
192
+ }
193
+ }
194
+ exprCache.set(expr, cached);
195
+ }
196
+ if (cached.kind === "path") {
197
+ if (!safeStateHas(state, cached.parts[0])) return void 0;
198
+ return cached.parts.reduce(
183
199
  (obj, key) => obj != null ? obj[key] : void 0,
184
200
  state
185
201
  );
186
202
  }
187
- if (!exprCache.has(expr)) {
188
- try {
189
- exprCache.set(
190
- expr,
191
- new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
192
- );
193
- } catch {
194
- warn(`invalid expression "${expr}"`);
195
- exprCache.set(expr, () => void 0);
196
- }
197
- }
198
203
  try {
199
- return exprCache.get(expr)(safeStateWrap(state), SAFE_OUTER);
204
+ return cached.fn(safeStateWrap(state), SAFE_OUTER);
200
205
  } catch (e) {
201
206
  if (!warnedRuntime.has(expr)) {
202
207
  warnedRuntime.add(expr);
@@ -234,11 +239,12 @@ function emit(event, payload) {
234
239
  }
235
240
 
236
241
  // src/core/reactive.ts
237
- function createReactiveState(obj, schedule) {
242
+ function createReactiveState(obj, schedule, onKey) {
238
243
  return new Proxy(obj, {
239
244
  set(target, key, value) {
240
245
  ;
241
246
  target[key] = value;
247
+ onKey == null ? void 0 : onKey(key);
242
248
  schedule();
243
249
  return true;
244
250
  }
@@ -264,7 +270,8 @@ function applyText(el, expr, state) {
264
270
  }
265
271
  function applyHtml(el, expr, state) {
266
272
  var _a;
267
- el.innerHTML = String((_a = evalExpr(expr, state)) != null ? _a : "");
273
+ const html = String((_a = evalExpr(expr, state)) != null ? _a : "");
274
+ if (el.innerHTML !== html) el.innerHTML = html;
268
275
  }
269
276
  function applyIf(binding, state) {
270
277
  const el = binding.el;
@@ -281,7 +288,9 @@ function applyIf(binding, state) {
281
288
  }
282
289
  }
283
290
  function applyShow(el, expr, state) {
284
- el.style.display = evalExpr(expr, state) ? "" : "none";
291
+ const desired = evalExpr(expr, state) ? "" : "none";
292
+ const htmlEl = el;
293
+ if (htmlEl.style.display !== desired) htmlEl.style.display = desired;
285
294
  }
286
295
  function applyBind(el, pairs, state) {
287
296
  for (const [attr, valExpr] of pairs) {
@@ -542,7 +551,7 @@ function scanFragment(frag) {
542
551
  }
543
552
 
544
553
  // src/dom/each.ts
545
- function renderList(templates, state, rawState, instance) {
554
+ function renderList(templates, state, rawState, instance, triggerKey) {
546
555
  var _a;
547
556
  for (const tmplEl of templates) {
548
557
  if (tmplEl.tagName !== "TEMPLATE") continue;
@@ -559,22 +568,22 @@ function renderList(templates, state, rawState, instance) {
559
568
  }
560
569
  const marker = tmpl.__micraMarker;
561
570
  const keyMap = tmpl.__micraNodes;
562
- const parent = marker.parentNode;
563
- if (!parent) continue;
571
+ if (!marker.parentNode) continue;
564
572
  if (!Array.isArray(items)) {
565
573
  tmpl.__micraList.forEach((n) => n.remove());
566
574
  tmpl.__micraList = [];
567
575
  keyMap.clear();
568
576
  continue;
569
577
  }
578
+ const canSkipUnchanged = triggerKey !== null && triggerKey !== "MULTIPLE" && triggerKey === itemsExpr;
570
579
  if (keyAttr) {
571
- renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance);
580
+ renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
572
581
  } else {
573
- renderNoKey(tmpl, items, marker, parent, state, rawState, instance);
582
+ renderNoKey(tmpl, items, marker, state, rawState, instance);
574
583
  }
575
584
  }
576
585
  }
577
- function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
586
+ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged) {
578
587
  var _a;
579
588
  const nextKeys = /* @__PURE__ */ new Set();
580
589
  const nextNodes = [];
@@ -608,11 +617,17 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
608
617
  bindDataOn(rowScan2.on, instance);
609
618
  bindAtEvents(rowScan2.atEvents, instance);
610
619
  bindModels(rowScan2.model, instance);
620
+ node._itemState = Object.create(state);
621
+ } else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
622
+ nextNodes.push(node);
623
+ continue;
611
624
  }
612
- const itemState = Object.assign(
613
- Object.create(state),
614
- { item, index, $index: index }
615
- );
625
+ node.__micraItem = item;
626
+ node.__micraIndex = index;
627
+ const itemState = node._itemState;
628
+ itemState.item = item;
629
+ itemState.index = index;
630
+ itemState.$index = index;
616
631
  const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
617
632
  applyDirectives(rowScan, itemState, rawState, instance);
618
633
  nextNodes.push(node);
@@ -623,14 +638,64 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
623
638
  keyMap.delete(key);
624
639
  }
625
640
  }
626
- let cursor = marker;
627
- for (const node of nextNodes) {
628
- if (cursor.nextSibling !== node) parent.insertBefore(node, cursor.nextSibling);
629
- cursor = node;
641
+ const prevList = tmpl.__micraList;
642
+ if (prevList.length === 0) {
643
+ if (nextNodes.length) {
644
+ const frag = document.createDocumentFragment();
645
+ for (const node of nextNodes) frag.append(node);
646
+ marker.after(frag);
647
+ }
648
+ } else {
649
+ let orderChanged = nextNodes.length !== prevList.length;
650
+ if (!orderChanged) {
651
+ for (let i = 0; i < nextNodes.length; i++) {
652
+ if (nextNodes[i] !== prevList[i]) {
653
+ orderChanged = true;
654
+ break;
655
+ }
656
+ }
657
+ }
658
+ if (orderChanged) reorderKeyed(nextNodes, prevList, marker);
630
659
  }
631
660
  tmpl.__micraList = nextNodes;
632
661
  }
633
- function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
662
+ function reorderKeyed(nextNodes, prevList, marker) {
663
+ const prevPos = /* @__PURE__ */ new Map();
664
+ for (let i = 0; i < prevList.length; i++) prevPos.set(prevList[i], i);
665
+ const n = nextNodes.length;
666
+ const tails = [];
667
+ const tailIdx = [];
668
+ const prev = new Array(n).fill(-1);
669
+ for (let i = 0; i < n; i++) {
670
+ const p = prevPos.get(nextNodes[i]);
671
+ if (p === void 0) continue;
672
+ let lo = 0, hi = tails.length;
673
+ while (lo < hi) {
674
+ const m = lo + hi >> 1;
675
+ tails[m] < p ? lo = m + 1 : hi = m;
676
+ }
677
+ if (lo > 0) prev[i] = tailIdx[lo - 1];
678
+ tails[lo] = p;
679
+ tailIdx[lo] = i;
680
+ }
681
+ const stable = /* @__PURE__ */ new Set();
682
+ let idx = tailIdx[tails.length - 1];
683
+ while (idx >= 0) {
684
+ stable.add(idx);
685
+ idx = prev[idx];
686
+ }
687
+ let anchor = marker;
688
+ for (let i = 0; i < n; i++) {
689
+ const node = nextNodes[i];
690
+ if (stable.has(i)) {
691
+ anchor = node;
692
+ continue;
693
+ }
694
+ anchor.after(node);
695
+ anchor = node;
696
+ }
697
+ }
698
+ function renderNoKey(tmpl, items, marker, state, rawState, instance) {
634
699
  tmpl.__micraList.forEach((n) => n.remove());
635
700
  tmpl.__micraList = [];
636
701
  const frag = document.createDocumentFragment();
@@ -652,11 +717,12 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
652
717
  });
653
718
  tmpl.__micraList.push(...nodes);
654
719
  }
655
- parent.insertBefore(frag, marker.nextSibling);
720
+ marker.after(frag);
656
721
  }
657
722
 
658
723
  // src/dom/refs.ts
659
724
  function collectRefs(els, instance) {
725
+ if (!els.length) return;
660
726
  instance.refs = {};
661
727
  for (const el of els) {
662
728
  const name = el.dataset["ref"];
@@ -699,8 +765,12 @@ function mount(selector, definition) {
699
765
  return unsub;
700
766
  };
701
767
  let isRendering = false;
768
+ let _triggerKey = null;
702
769
  const schedule = createScheduler(() => instance.render());
703
- instance.state = createReactiveState(rawState, schedule);
770
+ instance.state = createReactiveState(rawState, schedule, (key) => {
771
+ if (_triggerKey === null) _triggerKey = key;
772
+ else if (_triggerKey !== key) _triggerKey = "MULTIPLE";
773
+ });
704
774
  const boundMethods = /* @__PURE__ */ new Map();
705
775
  const exprState = new Proxy(rawState, {
706
776
  get(target, key) {
@@ -724,6 +794,8 @@ function mount(selector, definition) {
724
794
  instance.render = function() {
725
795
  var _a2;
726
796
  if (instance.__micraDestroyed) return;
797
+ const triggerKey = _triggerKey;
798
+ _triggerKey = null;
727
799
  if (isRendering) {
728
800
  if (!warnedReentry) {
729
801
  warn(
@@ -738,7 +810,7 @@ function mount(selector, definition) {
738
810
  const mRoot2 = root;
739
811
  const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
740
812
  applyDirectives(scan, exprState, rawState, instance);
741
- renderList(scan.each, exprState, rawState, instance);
813
+ renderList(scan.each, exprState, rawState, instance, triggerKey);
742
814
  bindDataOn(scan.on, instance);
743
815
  bindAtEvents(scan.atEvents, instance);
744
816
  bindModels(scan.model, instance);