You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Status — as of #42 (merged): substantially implemented. The memoization / re-render-cascade work is done; what remains is fresh-source-object-per-render robustness. Kept open for the items below. (Original report preserved further down; its line numbers are pre-#42.)
Remaining work
Replace the full JSON.stringify(source) sameness check (activity-viewer.tsx:95-96) with a reference/shallow comparison or a required source id. It is now memoized, so it is skipped while source identity is stable — but a consumer that passes a fresh (new-but-equal) source object each render still serializes the entire assignment every render. The same O(assignment-size)-per-render residual applies to createSourceHash (object-hash over the full source, now memoized at Viewer.tsx:83-100).
Key the reducer re-init on the stable sourceHash value, not source identity. The initialize effect depends on source (Viewer.tsx:234-254) and ActivityViewer passes the raw source straight through (activity-viewer.tsx:132-133), so a fresh-object consumer can still trigger a reducer re-initialization per render. Note the content-based propSetKey sameness pattern was applied to variant regeneration but not extended to gate this reducer init.
Document that consumers should pass a stable source identity (README and/or the ActivityViewer prop docs) — not yet added.
Memoize the Activity tree — Activity / SequenceActivity / SelectActivity / SingleDocActivity are now memo-wrapped and callbacks are stabilized (Problem 1).
Extract per-item derived props (answerResponseCounts / doenetStates / itemAttemptNumbers) at the Activity→SingleDoc boundary via itemIndexById.get(...) (Activity.tsx:56-67), so one item's score report no longer re-renders all N item subtrees (Problem 3). The iframe-reload risk it called out is separately eliminated by "apply prop changes without reloading the iframe" (commit ace19821a).
Memoize the source serialization/hash — JSON.stringify(source) and createSourceHash(source) now run only when source identity changes rather than literally every render (partial Problems 2 & 4; see Remaining work for the fresh-object case).
Problem
Several patterns cause the whole Activity tree to re-render (and risk iframe churn) on every state update:
No React.memo anywhere in the tree (Activity.tsx, SequenceActivity.tsx, SelectActivity.tsx, SingleDocActivity.tsx): every reducer dispatch — each score report, each navigation — re-renders all N item subtrees.
JSON.stringify(source) on every ActivityViewer render (activity-viewer.tsx:98) for prop-change detection: serializes the entire assignment, including every inline DoenetML string, per render.
createSourceHash (object-hash over the full source, activityState.ts:777) re-runs whenever the source prop identity changes; consumers passing inline objects pay it per render plus a reducer re-init via the effect at Viewer.tsx:169-180.
Proposed change
Memoize the Activity components; stabilize reportScoreAndStateCallback, checkRender, checkHidden etc. with useCallback.
Replace the stringify-compare with reference or shallow comparison (or require a source id).
Memoize/stabilize per-item derived props like answerResponseCounts; document that consumers should pass stable source identities.
Impact
Removes O(N) reconciliation on every keystroke-driven save and eliminates a class of accidental iframe reloads. Complements the mounting fixes (conditional mount, bounded window) — with fewer mounted viewers the cascades matter less, but the stringify-per-render cost remains proportional to assignment size either way.
Remaining work
JSON.stringify(source)sameness check (activity-viewer.tsx:95-96) with a reference/shallow comparison or a required source id. It is now memoized, so it is skipped whilesourceidentity is stable — but a consumer that passes a fresh (new-but-equal)sourceobject each render still serializes the entire assignment every render. The same O(assignment-size)-per-render residual applies tocreateSourceHash(object-hash over the full source, now memoized atViewer.tsx:83-100).sourceHashvalue, notsourceidentity. Theinitializeeffect depends onsource(Viewer.tsx:234-254) andActivityViewerpasses the rawsourcestraight through (activity-viewer.tsx:132-133), so a fresh-object consumer can still trigger a reducer re-initialization per render. Note the content-basedpropSetKeysameness pattern was applied to variant regeneration but not extended to gate this reducer init.sourceidentity (README and/or theActivityViewerprop docs) — not yet added.Done in #42
Activity/SequenceActivity/SelectActivity/SingleDocActivityare nowmemo-wrapped and callbacks are stabilized (Problem 1).answerResponseCounts/doenetStates/itemAttemptNumbers) at the Activity→SingleDoc boundary viaitemIndexById.get(...)(Activity.tsx:56-67), so one item's score report no longer re-renders all N item subtrees (Problem 3). The iframe-reload risk it called out is separately eliminated by "apply prop changes without reloading the iframe" (commit ace19821a).JSON.stringify(source)andcreateSourceHash(source)now run only whensourceidentity changes rather than literally every render (partial Problems 2 & 4; see Remaining work for the fresh-object case).Problem
Several patterns cause the whole Activity tree to re-render (and risk iframe churn) on every state update:
React.memoanywhere in the tree (Activity.tsx,SequenceActivity.tsx,SelectActivity.tsx,SingleDocActivity.tsx): every reducer dispatch — each score report, each navigation — re-renders all N item subtrees.JSON.stringify(source)on everyActivityViewerrender (activity-viewer.tsx:98) for prop-change detection: serializes the entire assignment, including every inline DoenetML string, per render.answerResponseCounts={answerResponseCountsByItem[itemIdx]}(SingleDocActivity.tsx:176): if the consumer passes a fresh object each render, the prop identity changes — and with@doenet/doenetml-iframebaking props into srcDoc (doenetml-iframe: any prop change reloads the entire iframe because props are baked into srcDoc; send updates via messages instead DoenetML#1436), that can reload the iframe outright.createSourceHash(object-hash over the full source,activityState.ts:777) re-runs whenever thesourceprop identity changes; consumers passing inline objects pay it per render plus a reducer re-init via the effect atViewer.tsx:169-180.Proposed change
reportScoreAndStateCallback,checkRender,checkHiddenetc. withuseCallback.answerResponseCounts; document that consumers should pass stablesourceidentities.Impact
Removes O(N) reconciliation on every keystroke-driven save and eliminates a class of accidental iframe reloads. Complements the mounting fixes (conditional mount, bounded window) — with fewer mounted viewers the cascades matter less, but the stringify-per-render cost remains proportional to assignment size either way.
🤖 Generated with Claude Code