Context
PR #6587 (v4.42) changed componentShouldUpdate to fire once per changed @prop instead of once per render batch. The PR closed #2061 and treats the previous batched behavior as a bug.
While per-prop firing matches the documented contract of componentShouldUpdate, it has a noticeable performance impact on component libraries whose components carry many @Props. Every parent re-render now multiplies the lifecycle cost by N (number of changed props).
This isn't a request to revert #6587. It's a request to expose an opt-out for codebases that would prefer the previous batched semantics.
Impact
Consider a form-input component with 24 @Props, used 30 times on a single page:
- Before 4.42: 30 × 1 = 30
componentShouldUpdate invocations per parent re-render
- After 4.42: 30 × 24 = 720 invocations per parent re-render
For most apps this is invisible. For dense-form pages (settings panels, checkout, multi-field surveys), it surfaces as TBT/INP regressions after upgrading to ≥4.42.
Repro shape
@Component({ tag: 'my-input', shadow: true })
export class MyInput {
@Prop() value: string
@Prop() label: string
@Prop() placeholder: string
// ... 20 more @Prop
componentShouldUpdate() {
console.count('shouldUpdate')
return true
}
render() { return <input /> }
}
Render 50 instances, then assign el.value = next once. Pre-4.42: 1 increment. Post-4.42: 24 increments — one per @prop that the runtime reconciles.
Proposed solutions (any one would help)
1. Component-level opt-in flag
@Component({
tag: 'my-input',
batchedShouldUpdate: true // restore pre-4.42 batched firing
})
Scoped, opt-in, no behavior change for existing users.
2. Global default componentShouldUpdate hook
stencil.config.ts:
export const config: Config = {
defaults: {
componentShouldUpdate(newVal, oldVal, propName) {
// applied to every component that does not define its own hook
return /* ... */
}
}
}
Solves the "N components" problem at config level. A library of 60 components becomes one config block instead of 60 method definitions.
3. Mixin support (#6584)
If v5 lands Mixin support, a WithPerfBail mixin would cover this:
@Component({ tag: 'my-input', shadow: true })
export class MyInput extends WithPerfBail(BaseInput) { ... }
Current workaround
Adding a default componentShouldUpdate to each high-prop component manually:
- Bail when
Object.is(newVal, oldVal) (reference equality on stable callbacks/values)
- Bail on known render-irrelevant prop names (
data-testid, analytics/tracking keys)
- Otherwise return true
Mechanical (~10 lines per component) but spreads across N files and is easy to miss for new components. A build-time codemod over the compiled output is feasible but fragile against future runtime changes.
Why this matters
Stencil libraries are exactly the case where this scales worst — sites with 30–100 @Components consumed by Angular/React/Vue apps. The cost is (component count) × (prop count) × (parent re-render frequency). For form-heavy SaaS, all three multipliers are high.
A batchedShouldUpdate: true flag (option 1) would be enough for affected consumers to opt back in, with zero impact on anyone relying on the new per-prop contract.
Context
PR #6587 (v4.42) changed
componentShouldUpdateto fire once per changed @prop instead of once per render batch. The PR closed #2061 and treats the previous batched behavior as a bug.While per-prop firing matches the documented contract of
componentShouldUpdate, it has a noticeable performance impact on component libraries whose components carry many@Props. Every parent re-render now multiplies the lifecycle cost byN(number of changed props).This isn't a request to revert #6587. It's a request to expose an opt-out for codebases that would prefer the previous batched semantics.
Impact
Consider a form-input component with 24
@Props, used 30 times on a single page:componentShouldUpdateinvocations per parent re-renderFor most apps this is invisible. For dense-form pages (settings panels, checkout, multi-field surveys), it surfaces as TBT/INP regressions after upgrading to ≥4.42.
Repro shape
Render 50 instances, then assign
el.value = nextonce. Pre-4.42: 1 increment. Post-4.42: 24 increments — one per @prop that the runtime reconciles.Proposed solutions (any one would help)
1. Component-level opt-in flag
Scoped, opt-in, no behavior change for existing users.
2. Global default
componentShouldUpdatehookstencil.config.ts:Solves the "N components" problem at config level. A library of 60 components becomes one config block instead of 60 method definitions.
3. Mixin support (#6584)
If v5 lands Mixin support, a
WithPerfBailmixin would cover this:Current workaround
Adding a default
componentShouldUpdateto each high-prop component manually:Object.is(newVal, oldVal)(reference equality on stable callbacks/values)data-testid, analytics/tracking keys)Mechanical (~10 lines per component) but spreads across N files and is easy to miss for new components. A build-time codemod over the compiled output is feasible but fragile against future runtime changes.
Why this matters
Stencil libraries are exactly the case where this scales worst — sites with 30–100
@Components consumed by Angular/React/Vue apps. The cost is(component count) × (prop count) × (parent re-render frequency). For form-heavy SaaS, all three multipliers are high.A
batchedShouldUpdate: trueflag (option 1) would be enough for affected consumers to opt back in, with zero impact on anyone relying on the new per-prop contract.