From e5a1163519079c2e9d88c1794032286ba39e5c4e Mon Sep 17 00:00:00 2001 From: Wout Mertens Date: Sat, 8 Nov 2025 06:55:19 +0100 Subject: [PATCH] fix(core): journal per chain of rerenders This should ensure that related changes happen together. --- packages/qwik/src/core/client/chore-array.ts | 5 ++++ packages/qwik/src/core/client/vnode-diff.ts | 18 ++++++++---- packages/qwik/src/core/shared/scheduler.ts | 31 ++++++++++++-------- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/qwik/src/core/client/chore-array.ts b/packages/qwik/src/core/client/chore-array.ts index 5ed1b27ab98..f935f142fb4 100644 --- a/packages/qwik/src/core/client/chore-array.ts +++ b/packages/qwik/src/core/client/chore-array.ts @@ -32,6 +32,11 @@ export class ChoreArray extends Array { if (existing.$payload$ !== value.$payload$) { existing.$payload$ = value.$payload$; } + if (value.$type$ === ChoreType.COMPONENT && value.$extra$) { + if (!existing.$extra$) { + existing.$extra$ = value.$extra$; + } + } return idx; } diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index a25327021a7..958ba64b3d4 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -51,7 +51,6 @@ import { serializeAttribute } from '../shared/utils/styles'; import { isArray, type ValueOrPromise } from '../shared/utils/types'; import { trackSignalAndAssignHost } from '../use/use-core'; import { TaskFlags, cleanupTask, isTask } from '../use/use-task'; -import type { DomContainer } from './dom-container'; import { VNodeFlags, type ClientAttrs, type ClientContainer } from './types'; import { mapApp_findIndx, mapArray_set } from './util-mapArray'; import { @@ -82,14 +81,16 @@ import { import type { ElementVNode, TextVNode, VNode, VirtualVNode } from './vnode-impl'; import { getAttributeNamespace, getNewElementNamespaceData } from './vnode-namespace'; +export type VNodeJournalRef = { $journal$: VNodeJournal; $refCount$: number }; + export const vnode_diff = ( container: ClientContainer, jsxNode: JSXChildren, vStartNode: VNode, - scopedStyleIdPrefix: string | null + scopedStyleIdPrefix: string | null, + journalRef: VNodeJournalRef = { $journal$: [], $refCount$: 1 } ) => { - let journal = (container as DomContainer).$journal$; - + let journal = journalRef.$journal$; /** * Stack is used to keep track of the state of the traversal. * @@ -231,6 +232,7 @@ export const vnode_diff = ( } } else if (jsxValue === (SkipRender as JSXChildren)) { // do nothing, we are skipping this node + // Note, this probably breaks async stuff journal = []; } else { expectText(''); @@ -559,6 +561,11 @@ export const vnode_diff = ( diff(jsxNode, vHostNode); } } + journalRef.$refCount$--; + if (journalRef.$refCount$ === 0) { + // all done with the journal, pass it to the container + container.$journal$.push(...journalRef.$journal$); + } } function expectNoChildren() { @@ -1268,7 +1275,8 @@ export const vnode_diff = ( * deleted. */ (host as VirtualVNode).flags &= ~VNodeFlags.Deleted; - container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, vNodeProps); + journalRef.$refCount$++; + container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, vNodeProps, journalRef); } } descendContentToProject(jsxNode.children, host); diff --git a/packages/qwik/src/core/shared/scheduler.ts b/packages/qwik/src/core/shared/scheduler.ts index 870095f59df..ad87adc151c 100644 --- a/packages/qwik/src/core/shared/scheduler.ts +++ b/packages/qwik/src/core/shared/scheduler.ts @@ -80,13 +80,17 @@ * declaration order within component. */ +import { ChoreArray, choreComparator } from '../client/chore-array'; import { type DomContainer } from '../client/dom-container'; import { VNodeFlags, type ClientContainer } from '../client/types'; import { VNodeJournalOpCode, vnode_isVNode } from '../client/vnode'; -import { vnode_diff } from '../client/vnode-diff'; +import { vnode_diff, type VNodeJournalRef } from '../client/vnode-diff'; +import type { ElementVNode, VirtualVNode } from '../client/vnode-impl'; +import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl'; import { ComputedSignalImpl } from '../reactive-primitives/impl/computed-signal-impl'; import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl'; import { isSignal, type Signal } from '../reactive-primitives/signal.public'; +import { isSsrNode } from '../reactive-primitives/subscriber'; import type { NodePropPayload } from '../reactive-primitives/subscription-data'; import { SignalFlags, @@ -97,6 +101,7 @@ import { } from '../reactive-primitives/types'; import { scheduleEffects } from '../reactive-primitives/utils'; import { type ISsrNode, type SSRContainer } from '../ssr/ssr-types'; +import { invoke, newInvokeContext } from '../use/use-core'; import { runResource, type ResourceDescriptor } from '../use/use-resource'; import { Task, @@ -110,23 +115,18 @@ import { executeComponent } from './component-execution'; import type { OnRenderFn } from './component.public'; import type { Props } from './jsx/jsx-runtime'; import type { JSXOutput } from './jsx/types/jsx-node'; +import { createNextTick } from './platform/next-tick'; import { isServerPlatform } from './platform/platform'; import { type QRLInternal } from './qrl/qrl-class'; +import { findBlockingChore, findBlockingChoreForVisible } from './scheduler-rules'; import { SsrNodeFlags, type Container, type HostElement } from './types'; import { ChoreType } from './util-chore-type'; +import { logWarn } from './utils/log'; import { QScopedStyle } from './utils/markers'; import { isPromise, maybeThen, retryOnPromise, safeCall } from './utils/promises'; import { addComponentStylePrefix } from './utils/scoped-styles'; import { serializeAttribute } from './utils/styles'; import { type ValueOrPromise } from './utils/types'; -import { invoke, newInvokeContext } from '../use/use-core'; -import { findBlockingChore, findBlockingChoreForVisible } from './scheduler-rules'; -import { createNextTick } from './platform/next-tick'; -import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl'; -import { isSsrNode } from '../reactive-primitives/subscriber'; -import { logWarn } from './utils/log'; -import type { ElementVNode, VirtualVNode } from '../client/vnode-impl'; -import { ChoreArray, choreComparator } from '../client/chore-array'; // Turn this on to get debug output of what the scheduler is doing. const DEBUG: boolean = false; @@ -161,6 +161,7 @@ export interface Chore { $resolve$: ((value: any) => void) | undefined; $reject$: ((reason?: any) => void) | undefined; $returnValue$: ValueOrPromise>; + $extra$?: unknown; } export type Scheduler = ReturnType; @@ -243,7 +244,8 @@ export const createScheduler = ( type: ChoreType.COMPONENT, host: HostElement, qrl: QRLInternal>, - props: Props | null + props: Props | null, + journalRef?: VNodeJournalRef ): Chore; function schedule( type: ChoreType.NODE_DIFF, @@ -263,7 +265,8 @@ export const createScheduler = ( type: T, hostOrTask: HostElement | Task | null = null, targetOrQrl: ChoreTarget | string | null = null, - payload: any = null + payload: unknown = null, + extra?: unknown ): Chore | null { if (type === ChoreType.WAIT_FOR_QUEUE && drainChore) { return drainChore as Chore; @@ -293,6 +296,9 @@ export const createScheduler = ( $reject$: undefined, $returnValue$: null!, }; + if (extra !== undefined) { + (chore as any).$extra$ = extra; + } if (type === ChoreType.WAIT_FOR_QUEUE) { getChorePromise(chore); @@ -659,7 +665,8 @@ This is often caused by modifying a signal in an already rendered component duri container as ClientContainer, jsx, host as VirtualVNode, - addComponentStylePrefix(styleScopedId) + addComponentStylePrefix(styleScopedId), + (chore as any).$extra$ ) ); }