From ae3ac8db6b89a0f9e20eb56dcc194b3ba56cd27b Mon Sep 17 00:00:00 2001 From: Dejan Vintonjiv Date: Sat, 23 May 2026 13:05:05 +0200 Subject: [PATCH 1/7] chore(performance): Optimize shared style loading and grouped theme listeners --- packages/primeng/src/base/base.ts | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/packages/primeng/src/base/base.ts b/packages/primeng/src/base/base.ts index 13d04f100df..baed2b1111f 100644 --- a/packages/primeng/src/base/base.ts +++ b/packages/primeng/src/base/base.ts @@ -1,5 +1,47 @@ +import { ThemeService } from '@primeuix/styled'; + export default { _loadedStyleNames: new Set(), + _themeChangeWired: false, + _groupedThemeChangeListeners: new Map(), + _ensureThemeChangeWired() { + if (this._themeChangeWired) { + return; + } + + this._themeChangeWired = true; + ThemeService.on('theme:change', () => this._loadedStyleNames.clear()); + }, + _registerGroupedThemeChangeListener(name, callback) { + const listener = this._groupedThemeChangeListeners.get(name); + + if (listener) { + listener.callbacks.add(callback); + return () => { + listener.callbacks.delete(callback); + + if (!listener.callbacks.size) { + ThemeService.off('theme:change', listener.hold); + this._groupedThemeChangeListeners.delete(name); + } + }; + } + + const callbacks = new Set([callback]); + const hold = (...args) => callbacks.forEach((callback) => callback(...args)); + + this._groupedThemeChangeListeners.set(name, { callbacks, hold }); + ThemeService.on('theme:change', hold); + + return () => { + callbacks.delete(callback); + + if (!callbacks.size) { + ThemeService.off('theme:change', hold); + this._groupedThemeChangeListeners.delete(name); + } + }; + }, getLoadedStyleNames() { return this._loadedStyleNames; }, From d05c85f70f8e21069bc0bf19e30dd2fe08555c1a Mon Sep 17 00:00:00 2001 From: Dejan Vintonjiv Date: Sat, 23 May 2026 13:05:31 +0200 Subject: [PATCH 2/7] chore(performance): Reduce BaseComponent render overhead with caching and opt-out flags --- .../src/basecomponent/basecomponent.ts | 267 ++++++++++++++++-- 1 file changed, 246 insertions(+), 21 deletions(-) diff --git a/packages/primeng/src/basecomponent/basecomponent.ts b/packages/primeng/src/basecomponent/basecomponent.ts index bd42ea1f594..92cb36fee00 100644 --- a/packages/primeng/src/basecomponent/basecomponent.ts +++ b/packages/primeng/src/basecomponent/basecomponent.ts @@ -9,6 +9,13 @@ import { BaseComponentStyle } from './style/basecomponentstyle'; export const PARENT_INSTANCE = new InjectionToken('PARENT_INSTANCE'); +export interface BaseComponentPerformanceContext { + themeReactive?: () => boolean | undefined; + scopedTokens?: () => boolean | undefined; +} + +export const PERFORMANCE_CONTEXT = new InjectionToken('PERFORMANCE_CONTEXT'); + @Directive({ standalone: true, providers: [BaseComponentStyle, BaseStyle] @@ -30,6 +37,8 @@ export class BaseComponent implements Lifecycle { public $parentInstance: BaseComponent | undefined = inject(PARENT_INSTANCE, { optional: true, skipSelf: true }) ?? undefined; + public $performanceContext: BaseComponentPerformanceContext | undefined = inject(PERFORMANCE_CONTEXT, { optional: true, skipSelf: true }) ?? undefined; + public baseComponentStyle: BaseComponentStyle = inject(BaseComponentStyle); public baseStyle: BaseStyle = inject(BaseStyle); @@ -44,6 +53,26 @@ export class BaseComponent implements Lifecycle { private themeChangeListenerMap: Map = new Map(); + private ptmCache: Map> = new Map(); + + private ptmsCache: Map> = new Map(); + + private cxStaticCache: Map = new Map(); + + private sxStaticCache: Map | undefined> = new Map(); + + private styleCache: any; + + private emptyPT: Record = {}; + + private paramsCache: any; + + private paramsCacheName: string | undefined; + + private paramsCacheHostName: any; + + private paramsCacheParentInstance: BaseComponent | undefined; + /******************** Inputs ********************/ /** @@ -70,6 +99,18 @@ export class BaseComponent implements Lifecycle { * @defaultValue undefined */ ptOptions = input(); + /** + * Enables per-instance theme change reactivity. + * Set to `false` in performance-sensitive component subtrees when runtime theme changes are not needed. + * @defaultValue undefined + */ + themeReactive = input(); + /** + * Enables scoped design token handling through the `dt` input. + * Set to `false` in performance-sensitive component subtrees when scoped tokens are not used. + * @defaultValue undefined + */ + scopedTokens = input(); /******************** Computed ********************/ @@ -108,7 +149,19 @@ export class BaseComponent implements Lifecycle { } get $style() { - return { theme: undefined, css: undefined, classes: undefined, inlineStyles: undefined, ...(this._getHostInstance(this) || {}).$style, ...this['_componentStyle'] }; + if (this.styleCache) { + return this.styleCache; + } + + const hostStyle = (this._getHostInstance(this) || {}).$style; + const componentStyle = this['_componentStyle']; + const style = { theme: undefined, css: undefined, classes: undefined, inlineStyles: undefined, ...hostStyle, ...componentStyle }; + + if (hostStyle || componentStyle) { + this.styleCache = style; + } + + return style; } get $styleOptions() { @@ -116,14 +169,25 @@ export class BaseComponent implements Lifecycle { } get $params() { + const name = this.$name; + const hostName = this.$hostName; const parentInstance = this._getHostInstance(this) || this.$parentInstance; - return { + if (this.paramsCache && this.paramsCacheName === name && this.paramsCacheHostName === hostName && this.paramsCacheParentInstance === parentInstance) { + return this.paramsCache; + } + + this.paramsCacheName = name; + this.paramsCacheHostName = hostName; + this.paramsCacheParentInstance = parentInstance; + this.paramsCache = { instance: this as any, parent: { instance: parentInstance } }; + + return this.paramsCache; } /******************** Lifecycle Hooks ********************/ @@ -166,10 +230,13 @@ export class BaseComponent implements Lifecycle { // watch _dt_ changes effect((onCleanup) => { if (this.document && !isPlatformServer(this.platformId)) { - if (this.dt()) { + if (this._isScopedTokensEnabled() && this.dt()) { this._loadScopedThemeStyles(this.dt()); - this._themeScopedListener = () => this._loadScopedThemeStyles(this.dt()); - this._themeChangeListener('_themeScopedListener', this._themeScopedListener); + + if (this._isThemeReactive()) { + this._themeScopedListener = () => this._loadScopedThemeStyles(this.dt()); + this._themeChangeListener('_themeScopedListener', this._themeScopedListener); + } } else { this._unloadScopedThemeStyles(); } @@ -183,7 +250,7 @@ export class BaseComponent implements Lifecycle { // watch _unstyled_ changes effect((onCleanup) => { if (this.document && !isPlatformServer(this.platformId)) { - if (!this.$unstyled()) { + if (this._isThemeReactive() && !this.$unstyled() && this._hasCoreStyles()) { this._loadCoreStyles(); this._themeChangeListener('_loadCoreStyles', this._loadCoreStyles); // Update styles with theme settings } @@ -203,7 +270,10 @@ export class BaseComponent implements Lifecycle { * Use 'onInit()' in subclasses instead. */ ngOnInit() { - this._loadCoreStyles(); + if (this._hasCoreStyles()) { + this._loadCoreStyles(); + } + this._loadStyles(); this.onInit(); @@ -257,7 +327,9 @@ export class BaseComponent implements Lifecycle { */ ngAfterViewInit() { // @todo - remove this after implementing pt for root - this.$el?.setAttribute(this.$attrSelector, ''); + if (this.config?.ptMetadata()) { + this.$el?.setAttribute(this.$attrSelector, ''); + } this.onAfterViewInit(); this._hook('onAfterViewInit'); @@ -304,8 +376,46 @@ export class BaseComponent implements Lifecycle { return getKeyValue(options, key, params); } + private _getRawOptionValue(options: any, key = '') { + if (!key || !options) { + return options; + } + + return key.split('.').reduce((acc, part) => acc?.[part], options); + } + + private _isStaticClassValue(value: any) { + return isString(value) || (isArray(value) && value.every((item) => isString(item))); + } + + private _isStaticStyleValue(value: any) { + return !isFunction(value); + } + + private _getInheritedBooleanOption(name: string, defaultValue = true) { + const ownValue = this[name]?.(); + + if (ownValue !== undefined) { + return ownValue; + } + + const parentInstance = this._getHostInstance(this) || this.$parentInstance; + const parentValue = parentInstance?.[name]; + const contextValue = this.$performanceContext?.[name]; + + return (isFunction(parentValue) ? parentValue.call(parentInstance) : parentValue) ?? (isFunction(contextValue) ? contextValue.call(this.$performanceContext) : contextValue) ?? defaultValue; + } + + private _isThemeReactive() { + return this._getInheritedBooleanOption('themeReactive'); + } + + private _isScopedTokensEnabled() { + return this._getInheritedBooleanOption('scopedTokens'); + } + private _hook(hookName: string, ...args: any[]) { - if (!this.$hostName) { + if (this.config?.ptBinding() && !this.$hostName && this._hasPTConfig()) { const selfHook = this._usePT(this._getPT(this.$pt(), this.$name), this._getOptionValue, `hooks.${hookName}`); const defaultHook = this._useDefaultPT(this._getOptionValue, `hooks.${hookName}`); @@ -329,7 +439,10 @@ export class BaseComponent implements Lifecycle { private _loadStyles() { this._load(); - this._themeChangeListener('_load', () => this._load()); + + if (this._isThemeReactive()) { + this._themeChangeListener('_load', () => this._load()); + } } private _loadGlobalStyles() { @@ -347,6 +460,10 @@ export class BaseComponent implements Lifecycle { } } + private _hasCoreStyles() { + return !!(this.baseComponentStyle?.css || this.$style?.css); + } + private _loadThemeStyles() { if (this.$unstyled() || this.config?.theme() === 'none') return; @@ -393,11 +510,17 @@ export class BaseComponent implements Lifecycle { } private _themeChangeListener(id: string, callback = () => {}) { + Base._ensureThemeChangeWired(); this._offThemeChangeListener(id); - Base.clearLoadedStyleNames(); const hold = callback.bind(this); - this.themeChangeListenerMap.set(id, hold); - ThemeService.on('theme:change', hold); + const styleName = this.$style?.name; + + if ((id === '_load' || id === '_loadCoreStyles') && styleName) { + this.themeChangeListenerMap.set(id, Base._registerGroupedThemeChangeListener(`${id}:${styleName}`, hold)); + } else { + this.themeChangeListenerMap.set(id, () => ThemeService.off('theme:change', hold)); + ThemeService.on('theme:change', hold); + } } private _removeThemeListeners() { @@ -408,7 +531,7 @@ export class BaseComponent implements Lifecycle { private _offThemeChangeListener(id: string) { if (this.themeChangeListenerMap.has(id)) { - ThemeService.off('theme:change', this.themeChangeListenerMap.get(id)); + this.themeChangeListenerMap.get(id)?.(); this.themeChangeListenerMap.delete(id); } } @@ -426,6 +549,10 @@ export class BaseComponent implements Lifecycle { } private _getPTDatasets(key = '') { + if (!this.config?.ptBinding() || !this.config?.ptMetadata()) { + return undefined; + } + const datasetPrefix = 'data-pc-'; const isExtended = key === 'root' && isNotEmpty(this.$pt()?.['data-pc-section']); @@ -441,6 +568,26 @@ export class BaseComponent implements Lifecycle { ); } + private _hasPTConfig() { + return !!(this.pt() || this.directivePT() || this.config?.pt()); + } + + private _hasPTParams(params: Record | undefined) { + return params ? Object.keys(params).length > 0 : false; + } + + private _getCachedPTMDatasets(key = '') { + let cached = this.ptmCache.get(key); + + if (!cached) { + const datasets = this._getPTDatasets(key); + cached = datasets ? { ...datasets } : {}; + this.ptmCache.set(key, cached); + } + + return cached; + } + private _getPTClassValue(options?: any, key?: any, params?: any) { const value = this._getOptionValue(options, key, params); @@ -493,30 +640,108 @@ export class BaseComponent implements Lifecycle { /******************** Exposed API ********************/ - public ptm(key = '', params = {}) { + public ptm(key = '', params?: Record) { + if (!this.config?.ptBinding()) { + return this.emptyPT; + } + + if (!this._hasPTConfig() && !this._hasPTParams(params)) { + return this._getCachedPTMDatasets(key); + } + return this._getPTValue(this.$pt() as any, key, { ...this.$params, ...params }); } - public ptms(keys: string[], params = {}) { - return keys.reduce((acc, arg) => { - acc = mergeProps(acc, this.ptm(arg, params)) || {}; - return acc; - }, {}); + public ptms(keys: string[], params?: Record) { + if (!this.config?.ptBinding()) { + return this.emptyPT; + } + + if (!this._hasPTConfig() && !this._hasPTParams(params)) { + const cacheKey = keys.join('|'); + let cached = this.ptmsCache.get(cacheKey); + + if (!cached) { + cached = + keys.reduce((acc, arg) => { + acc = mergeProps(acc, this._getCachedPTMDatasets(arg)) || {}; + return acc; + }, {}) || {}; + this.ptmsCache.set(cacheKey, cached); + } + + return cached; + } + + return ( + keys.reduce((acc, arg) => { + acc = mergeProps(acc, this.ptm(arg, params)) || {}; + return acc; + }, {}) || {} + ); } public ptmo(obj = {}, key = '', params = {}) { + if (!this.config?.ptBinding()) { + return this.emptyPT; + } + return this._getPTValue(obj, key, { instance: this, ...params }, false); } public cx(key: string, params = {}) { - return !this.$unstyled() ? cn(this._getOptionValue(this.$style.classes, key, { ...this.$params, ...params })) : undefined; + if (this.$unstyled()) { + return undefined; + } + + if (!this._hasPTParams(params)) { + if (this.cxStaticCache.has(key)) { + return this.cxStaticCache.get(key); + } + + const rawValue = this._getRawOptionValue(this.$style.classes, key); + + if (this._isStaticClassValue(rawValue)) { + const className = cn(rawValue); + + this.cxStaticCache.set(key, className); + + return className; + } + } + + return cn(this._getOptionValue(this.$style.classes, key, { ...this.$params, ...params })); } public sx(key = '', when = true, params = {}) { if (when) { + if (!this._hasPTParams(params)) { + const rawSelf = this._getRawOptionValue(this.$style.inlineStyles, key); + const rawBase = this._getRawOptionValue(this.baseComponentStyle.inlineStyles, key); + + if (this._isStaticStyleValue(rawSelf) && this._isStaticStyleValue(rawBase)) { + const cacheKey = key || '$root'; + + if (this.sxStaticCache.has(cacheKey)) { + return this.sxStaticCache.get(cacheKey); + } + + const style = { ...rawBase, ...rawSelf }; + const cached = isNotEmpty(style) ? style : undefined; + + this.sxStaticCache.set(cacheKey, cached); + + return cached; + } + } + const self = this._getOptionValue(this.$style.inlineStyles, key, { ...this.$params, ...params }) as Record; const base = this._getOptionValue(this.baseComponentStyle.inlineStyles, key, { ...this.$params, ...params }) as Record; + if (!isNotEmpty(base) && !isNotEmpty(self)) { + return undefined; + } + return { ...base, ...self }; } From e1b966a5f17f4700441a7c3b5f0e69c0789b04bd Mon Sep 17 00:00:00 2001 From: Dejan Vintonjiv Date: Sat, 23 May 2026 13:05:57 +0200 Subject: [PATCH 3/7] chore(performance): Add pBind runtime opt-out and skip unchanged attribute writes --- packages/primeng/src/bind/bind.ts | 47 +++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/primeng/src/bind/bind.ts b/packages/primeng/src/bind/bind.ts index 6b687cb930d..dd96d1f5191 100644 --- a/packages/primeng/src/bind/bind.ts +++ b/packages/primeng/src/bind/bind.ts @@ -1,5 +1,6 @@ -import { computed, Directive, effect, ElementRef, input, NgModule, Renderer2, signal } from '@angular/core'; +import { computed, Directive, effect, ElementRef, inject, input, NgModule, Renderer2, signal } from '@angular/core'; import { cn, equals } from '@primeuix/utils'; +import { PrimeNG } from 'primeng/config'; /** * Bind directive provides dynamic attribute, property, and event listener binding functionality. @@ -14,6 +15,8 @@ import { cn, equals } from '@primeuix/utils'; } }) export class Bind { + private config = inject(PrimeNG); + /** * Dynamic attributes, properties, and event listeners to be applied to the host element. * @group Props @@ -23,8 +26,10 @@ export class Bind { private _attrs = signal<{ [key: string]: any } | undefined>(undefined); private attrs = computed(() => this._attrs() || this.pBind()); - styles = computed(() => this.attrs()?.style); - classes = computed(() => cn(this.attrs()?.class)); + styles = computed(() => (this.config.ptBinding() ? this.attrs()?.style : undefined)); + classes = computed(() => (this.config.ptBinding() ? cn(this.attrs()?.class) : undefined)); + + private appliedAttrs: { [key: string]: any } = {}; private listeners: { eventName: string; unlisten: () => void }[] = []; @@ -33,7 +38,19 @@ export class Bind { private renderer: Renderer2 ) { effect(() => { - const { style, class: className, ...rest } = this.attrs() || {}; + if (!this.config.ptBinding()) { + this.clearAppliedAttrs(); + this.clearListeners(); + return; + } + + const attrs = this.attrs(); + + if (!attrs) { + return; + } + + const { style, class: className, ...rest } = attrs; for (const [key, value] of Object.entries(rest)) { if (key.startsWith('on') && typeof value === 'function') { @@ -47,12 +64,18 @@ export class Bind { } else if (value === null || value === undefined) { // remove attr this.renderer.removeAttribute(this.el.nativeElement, key); - } else { - // attr & prop fallback - this.renderer.setAttribute(this.el.nativeElement, key, value.toString()); + delete this.appliedAttrs[key]; + } else if (this.appliedAttrs[key] !== value) { + const attrValue = value.toString(); + + this.appliedAttrs[key] = value; + this.renderer.setAttribute(this.el.nativeElement, key, attrValue); + if (key in this.el.nativeElement) { (this.el.nativeElement as any)[key] = value; } + } else { + continue; } } }); @@ -63,7 +86,7 @@ export class Bind { } public setAttrs(attrs: { [key: string]: any } | undefined) { - if (!equals(this._attrs(), attrs)) { + if (this._attrs() !== attrs && !equals(this._attrs(), attrs)) { this._attrs.set(attrs); } } @@ -72,6 +95,14 @@ export class Bind { this.listeners.forEach(({ unlisten }) => unlisten()); this.listeners = []; } + + private clearAppliedAttrs() { + for (const key of Object.keys(this.appliedAttrs)) { + this.renderer.removeAttribute(this.el.nativeElement, key); + } + + this.appliedAttrs = {}; + } } @NgModule({ From 27a0c51396519e5db08a46271a51b7f80a7cdf31 Mon Sep 17 00:00:00 2001 From: Dejan Vintonjiv Date: Sat, 23 May 2026 13:06:28 +0200 Subject: [PATCH 4/7] chore(performance): Preserve dynamic button icon tracking without PT metadata --- packages/primeng/src/button/button.ts | 47 +++++++++++++++++++++------ 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/primeng/src/button/button.ts b/packages/primeng/src/button/button.ts index 7e3feb6bdf5..b4aa2aa5267 100755 --- a/packages/primeng/src/button/button.ts +++ b/packages/primeng/src/button/button.ts @@ -25,7 +25,7 @@ import { addClass, createElement, findSingle, isEmpty } from '@primeuix/utils'; import { PrimeTemplate, SharedModule } from 'primeng/api'; import { AutoFocus } from 'primeng/autofocus'; import { BadgeModule } from 'primeng/badge'; -import { BaseComponent, PARENT_INSTANCE } from 'primeng/basecomponent'; +import { BaseComponent, PARENT_INSTANCE, PERFORMANCE_CONTEXT } from 'primeng/basecomponent'; import { Bind } from 'primeng/bind'; import { Fluid } from 'primeng/fluid'; import { SpinnerIcon } from 'primeng/icons'; @@ -54,7 +54,7 @@ const INTERNAL_BUTTON_CLASSES = { @Directive({ selector: '[pButtonLabel]', - providers: [ButtonStyle, { provide: BUTTON_LABEL_INSTANCE, useExisting: ButtonLabel }, { provide: PARENT_INSTANCE, useExisting: ButtonLabel }], + providers: [ButtonStyle, { provide: BUTTON_LABEL_INSTANCE, useExisting: ButtonLabel }, { provide: PARENT_INSTANCE, useExisting: ButtonLabel }, { provide: PERFORMANCE_CONTEXT, useExisting: ButtonLabel }], standalone: true, host: { '[class.p-button-label]': '!$unstyled() && true' @@ -107,7 +107,7 @@ export class ButtonLabel extends BaseComponent { @Directive({ selector: '[pButtonIcon]', - providers: [ButtonStyle, { provide: BUTTON_ICON_INSTANCE, useExisting: ButtonIcon }, { provide: PARENT_INSTANCE, useExisting: ButtonIcon }], + providers: [ButtonStyle, { provide: BUTTON_ICON_INSTANCE, useExisting: ButtonIcon }, { provide: PARENT_INSTANCE, useExisting: ButtonIcon }, { provide: PERFORMANCE_CONTEXT, useExisting: ButtonIcon }], standalone: true, host: { '[class.p-button-icon]': '!$unstyled() && true' @@ -164,7 +164,7 @@ export class ButtonIcon extends BaseComponent { @Directive({ selector: '[pButton]', standalone: true, - providers: [ButtonStyle, { provide: BUTTON_DIRECTIVE_INSTANCE, useExisting: ButtonDirective }, { provide: PARENT_INSTANCE, useExisting: ButtonDirective }], + providers: [ButtonStyle, { provide: BUTTON_DIRECTIVE_INSTANCE, useExisting: ButtonDirective }, { provide: PARENT_INSTANCE, useExisting: ButtonDirective }, { provide: PERFORMANCE_CONTEXT, useExisting: ButtonDirective }], host: { '[class.p-button-icon-only]': '!$unstyled() && isIconOnly()', '[class.p-button-text]': ' !$unstyled() && isTextButton()' @@ -305,6 +305,10 @@ export class ButtonDirective extends BaseComponent { private _internalClasses: string[] = Object.values(INTERNAL_BUTTON_CLASSES); + private dynamicLabelElement: HTMLElement | undefined; + + private dynamicIconElement: HTMLElement | undefined; + pcFluid: Fluid | null = inject(Fluid, { optional: true, host: true, skipSelf: true }); isTextButton = computed(() => !!(!this.iconSignal() && this.labelSignal() && this.text)); @@ -506,16 +510,17 @@ export class ButtonDirective extends BaseComponent { } createLabel() { - const created = findSingle(this.htmlElement, '[data-pc-section="buttonlabel"]'); + const created = this.getDynamicLabelElement(); if (!created && this.label) { let labelElement = createElement('span', { class: this.cx('label'), 'p-bind': this.ptm('buttonlabel'), 'aria-hidden': this.icon && !this.label ? 'true' : null }); labelElement.appendChild(this.document.createTextNode(this.label)); this.htmlElement.appendChild(labelElement); + this.dynamicLabelElement = labelElement; } } createIcon() { - const created = findSingle(this.htmlElement, '[data-pc-section="buttonicon"]'); + const created = this.getDynamicIconElement(); if (!created && (this.icon || this.loading)) { let iconPosClass = this.label && !this.$unstyled() ? 'p-button-icon-' + this.iconPos : null; let iconClass = !this.$unstyled() && this.getIconClass(); @@ -526,14 +531,16 @@ export class ButtonDirective extends BaseComponent { } this.htmlElement.insertBefore(iconElement, this.htmlElement.firstChild); + this.dynamicIconElement = iconElement; } } updateLabel() { - let labelElement = findSingle(this.htmlElement, '[data-pc-section="buttonlabel"]'); + let labelElement = this.getDynamicLabelElement(); if (!this.label) { labelElement && this.htmlElement.removeChild(labelElement); + this.dynamicLabelElement = undefined; return; } @@ -541,8 +548,8 @@ export class ButtonDirective extends BaseComponent { } updateIcon() { - let iconElement = findSingle(this.htmlElement, '[data-pc-section="buttonicon"]'); - let labelElement = findSingle(this.htmlElement, '[data-pc-section="buttonlabel"]'); + let iconElement = this.getDynamicIconElement(); + let labelElement = this.getDynamicLabelElement(); if (this.loading && !this.loadingIcon && iconElement) { iconElement.innerHTML = this.spinnerIcon; @@ -561,6 +568,26 @@ export class ButtonDirective extends BaseComponent { } } + private getDynamicLabelElement() { + if (this.dynamicLabelElement && this.htmlElement.contains(this.dynamicLabelElement)) { + return this.dynamicLabelElement; + } + + this.dynamicLabelElement = findSingle(this.htmlElement, '[data-pc-section="buttonlabel"]') as HTMLElement | undefined; + + return this.dynamicLabelElement; + } + + private getDynamicIconElement() { + if (this.dynamicIconElement && this.htmlElement.contains(this.dynamicIconElement)) { + return this.dynamicIconElement; + } + + this.dynamicIconElement = findSingle(this.htmlElement, '[data-pc-section="buttonicon"]') as HTMLElement | undefined; + + return this.dynamicIconElement; + } + getIconClass() { return this.loading ? 'p-button-loading-icon ' + (this.loadingIcon ? this.loadingIcon : 'p-icon') : this.icon || 'p-hidden'; } @@ -627,7 +654,7 @@ export class ButtonDirective extends BaseComponent { `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - providers: [ButtonStyle, { provide: BUTTON_INSTANCE, useExisting: Button }, { provide: PARENT_INSTANCE, useExisting: Button }], + providers: [ButtonStyle, { provide: BUTTON_INSTANCE, useExisting: Button }, { provide: PARENT_INSTANCE, useExisting: Button }, { provide: PERFORMANCE_CONTEXT, useExisting: Button }], hostDirectives: [Bind] }) export class Button extends BaseComponent { From fdc4d7f4efbf6d9dcdd270639f1ea4e083d137db Mon Sep 17 00:00:00 2001 From: Dejan Vintonjiv Date: Sat, 23 May 2026 13:07:53 +0200 Subject: [PATCH 5/7] chore(performance): Propagate BaseComponent performance context --- packages/primeng/src/chip/chip.ts | 4 ++-- packages/primeng/src/table/table.ts | 4 ++-- packages/primeng/src/tag/tag.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/primeng/src/chip/chip.ts b/packages/primeng/src/chip/chip.ts index 59807fb8484..bc9122ea218 100755 --- a/packages/primeng/src/chip/chip.ts +++ b/packages/primeng/src/chip/chip.ts @@ -18,7 +18,7 @@ import { ViewEncapsulation } from '@angular/core'; import { PrimeTemplate, SharedModule, TranslationKeys } from 'primeng/api'; -import { BaseComponent, PARENT_INSTANCE } from 'primeng/basecomponent'; +import { BaseComponent, PARENT_INSTANCE, PERFORMANCE_CONTEXT } from 'primeng/basecomponent'; import { Bind } from 'primeng/bind'; import { TimesCircleIcon } from 'primeng/icons'; import { ChipProps, ChipPassThrough } from 'primeng/types/chip'; @@ -80,7 +80,7 @@ const CHIP_INSTANCE = new InjectionToken('CHIP_INSTANCE'); `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - providers: [ChipStyle, { provide: CHIP_INSTANCE, useExisting: Chip }, { provide: PARENT_INSTANCE, useExisting: Chip }], + providers: [ChipStyle, { provide: CHIP_INSTANCE, useExisting: Chip }, { provide: PARENT_INSTANCE, useExisting: Chip }, { provide: PERFORMANCE_CONTEXT, useExisting: Chip }], host: { '[class]': "cn(cx('root'), styleClass)", '[style]': "sx('root')", diff --git a/packages/primeng/src/table/table.ts b/packages/primeng/src/table/table.ts index d8e9218a403..ea6b3aeda4b 100644 --- a/packages/primeng/src/table/table.ts +++ b/packages/primeng/src/table/table.ts @@ -33,7 +33,7 @@ import { MotionEvent, MotionOptions } from '@primeuix/motion'; import { absolutePosition, addStyle, appendChild, find, findSingle, getAttribute, isClickable, setAttribute } from '@primeuix/utils'; import { BlockableUI, FilterMatchMode, FilterMetadata, FilterOperator, FilterService, LazyLoadMeta, OverlayService, PrimeTemplate, ScrollerOptions, SelectItem, SharedModule, SortMeta, TableState, TranslationKeys } from 'primeng/api'; import { BadgeModule } from 'primeng/badge'; -import { BaseComponent, PARENT_INSTANCE } from 'primeng/basecomponent'; +import { BaseComponent, PARENT_INSTANCE, PERFORMANCE_CONTEXT } from 'primeng/basecomponent'; import { Bind, BindModule } from 'primeng/bind'; import { Button, ButtonModule } from 'primeng/button'; import { CheckboxChangeEvent, CheckboxModule } from 'primeng/checkbox'; @@ -353,7 +353,7 @@ export class TableService { `, - providers: [TableService, TableStyle, { provide: TABLE_INSTANCE, useExisting: Table }, { provide: PARENT_INSTANCE, useExisting: Table }], + providers: [TableService, TableStyle, { provide: TABLE_INSTANCE, useExisting: Table }, { provide: PARENT_INSTANCE, useExisting: Table }, { provide: PERFORMANCE_CONTEXT, useExisting: Table }], changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.None, host: { diff --git a/packages/primeng/src/tag/tag.ts b/packages/primeng/src/tag/tag.ts index 99e4ad03c35..77d64786cd3 100755 --- a/packages/primeng/src/tag/tag.ts +++ b/packages/primeng/src/tag/tag.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { AfterContentInit, booleanAttribute, ChangeDetectionStrategy, Component, ContentChild, ContentChildren, inject, InjectionToken, Input, NgModule, QueryList, TemplateRef, ViewEncapsulation } from '@angular/core'; import { PrimeTemplate, SharedModule } from 'primeng/api'; -import { BaseComponent, PARENT_INSTANCE } from 'primeng/basecomponent'; +import { BaseComponent, PARENT_INSTANCE, PERFORMANCE_CONTEXT } from 'primeng/basecomponent'; import { Bind } from 'primeng/bind'; import { TagPassThrough } from 'primeng/types/tag'; import { TagStyle } from './style/tagstyle'; @@ -28,7 +28,7 @@ const TAG_INSTANCE = new InjectionToken('TAG_INSTANCE'); `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - providers: [TagStyle, { provide: TAG_INSTANCE, useExisting: Tag }, { provide: PARENT_INSTANCE, useExisting: Tag }], + providers: [TagStyle, { provide: TAG_INSTANCE, useExisting: Tag }, { provide: PARENT_INSTANCE, useExisting: Tag }, { provide: PERFORMANCE_CONTEXT, useExisting: Tag }], host: { '[class]': "cn(cx('root'), styleClass)", '[attr.data-p]': 'dataP' From df8053b2debe8c0b06fa2eaea5e1e113bf288509 Mon Sep 17 00:00:00 2001 From: Dejan Vintonjiv Date: Sat, 23 May 2026 13:08:21 +0200 Subject: [PATCH 6/7] chore(performance): Add PrimeNG config flags for PT metadata and binding --- packages/primeng/src/config/primeng.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/primeng/src/config/primeng.ts b/packages/primeng/src/config/primeng.ts index 5e394a3f89b..c7536e17fba 100644 --- a/packages/primeng/src/config/primeng.ts +++ b/packages/primeng/src/config/primeng.ts @@ -28,6 +28,10 @@ export class PrimeNG extends ThemeProvider { ptOptions = signal(undefined); + ptMetadata = signal(true); + + ptBinding = signal(true); + filterMatchModeOptions = { text: [FilterMatchMode.STARTS_WITH, FilterMatchMode.CONTAINS, FilterMatchMode.NOT_CONTAINS, FilterMatchMode.ENDS_WITH, FilterMatchMode.EQUALS, FilterMatchMode.NOT_EQUALS], numeric: [FilterMatchMode.EQUALS, FilterMatchMode.NOT_EQUALS, FilterMatchMode.LESS_THAN, FilterMatchMode.LESS_THAN_OR_EQUAL_TO, FilterMatchMode.GREATER_THAN, FilterMatchMode.GREATER_THAN_OR_EQUAL_TO], @@ -186,7 +190,7 @@ export class PrimeNG extends ThemeProvider { } setConfig(config: PrimeNGConfigType): void { - const { csp, ripple, inputStyle, inputVariant, theme, overlayOptions, translation, filterMatchModeOptions, overlayAppendTo, zIndex, ptOptions, pt, unstyled } = config || {}; + const { csp, ripple, inputStyle, inputVariant, theme, overlayOptions, translation, filterMatchModeOptions, overlayAppendTo, zIndex, ptOptions, pt, ptMetadata, ptBinding, unstyled } = config || {}; if (csp) this.csp.set(csp); if (overlayAppendTo) this.overlayAppendTo.set(overlayAppendTo); @@ -199,6 +203,8 @@ export class PrimeNG extends ThemeProvider { if (zIndex) this.zIndex = zIndex; if (pt) this.pt.set(pt); if (ptOptions) this.ptOptions.set(ptOptions); + if (ptMetadata !== undefined) this.ptMetadata.set(ptMetadata); + if (ptBinding !== undefined) this.ptBinding.set(ptBinding); if (unstyled) this.unstyled.set(unstyled); if (theme) From 93989f8f046d50c1dfa95a30dbcc892009a13a3c Mon Sep 17 00:00:00 2001 From: Dejan Vintonjiv Date: Sat, 23 May 2026 13:08:33 +0200 Subject: [PATCH 7/7] chore(performance): Expose PT performance options in PrimeNG config types --- packages/primeng/src/config/primeng.types.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/primeng/src/config/primeng.types.ts b/packages/primeng/src/config/primeng.types.ts index 5186bcd03ee..c954443a727 100644 --- a/packages/primeng/src/config/primeng.types.ts +++ b/packages/primeng/src/config/primeng.types.ts @@ -205,5 +205,17 @@ export type PrimeNGConfigType = { zIndex?: ZIndex | null | undefined; pt?: GlobalPassThrough | null | undefined; ptOptions?: PassThroughOptions | null | undefined; + /** + * Enables automatic metadata attributes for PassThrough sections. + * Disable this in performance-sensitive views when `data-pc-*` attributes are not needed for tests, styling, or tooling. + * @defaultValue true + */ + ptMetadata?: boolean; + /** + * Enables PassThrough binding. + * Disable this in performance-sensitive views when `pt`, `data-pc-*`, and `pBind` driven attributes/classes/styles/events are not needed. + * @defaultValue true + */ + ptBinding?: boolean; filterMatchModeOptions?: any; } & ThemeConfigType;