From 7932b1a598eebef046b8775430ffe047be2844d3 Mon Sep 17 00:00:00 2001 From: Bowen Date: Mon, 23 Jun 2025 12:02:11 +0800 Subject: [PATCH 1/2] fix(i18n): globalstateservice stateupdates memory leak --- projects/core/src/index.performance.ts | 2 +- projects/core/src/internal/decorators/i18n.spec.ts | 11 +++++++++++ projects/core/src/internal/decorators/i18n.ts | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/projects/core/src/index.performance.ts b/projects/core/src/index.performance.ts index b0ec6a2eb..81eb52e13 100644 --- a/projects/core/src/index.performance.ts +++ b/projects/core/src/index.performance.ts @@ -56,6 +56,6 @@ describe('performance', () => { import '@cds/core/toggle/register.js'; import '@cds/core/tree-view/register.js';`; - expect((await testBundleSize(bundle, { optimize: true })).kb).toBeLessThan(63.2); + expect((await testBundleSize(bundle, { optimize: true })).kb).toBeLessThan(63.3); }); }); diff --git a/projects/core/src/internal/decorators/i18n.spec.ts b/projects/core/src/internal/decorators/i18n.spec.ts index 0939ad9e0..deec5fd71 100644 --- a/projects/core/src/internal/decorators/i18n.spec.ts +++ b/projects/core/src/internal/decorators/i18n.spec.ts @@ -11,6 +11,7 @@ import { getI18nUpdateStrategy, I18nService, I18nElement, + GlobalStateService, } from '@cds/core/internal'; import { html, LitElement } from 'lit'; import { componentIsStable, createTestElement, removeTestElement } from '@cds/core/test'; @@ -189,3 +190,13 @@ describe('helpers', () => { }); }); }); + +describe('unsubscribe', () => { + it('should unsubscribe from its subscription when the element is removed', async () => { + const testElement = await createTestElement( + html` ` + ); + removeTestElement(testElement); + expect((GlobalStateService.stateUpdates as any).subscriptions.length).toBe(0); + }); +}); diff --git a/projects/core/src/internal/decorators/i18n.ts b/projects/core/src/internal/decorators/i18n.ts index 7ecf99629..96b350bfe 100644 --- a/projects/core/src/internal/decorators/i18n.ts +++ b/projects/core/src/internal/decorators/i18n.ts @@ -58,7 +58,7 @@ export function i18n() { const targetDisconnectedCallback: () => void = protoOrDescriptor.disconnectedCallback; function connectedCallback(this: any): void { - protoOrDescriptor.__i18nSub = GlobalStateService.stateUpdates.subscribe(update => { + this.__i18nSub = GlobalStateService.stateUpdates.subscribe(update => { if (update.key === 'i18nRegistry') { this.requestUpdate(name); } @@ -70,7 +70,7 @@ export function i18n() { } function disconnectedCallback(this: any) { - protoOrDescriptor.__i18nSub.unsubscribe(); + this.__i18nSub.unsubscribe(); if (targetDisconnectedCallback) { targetDisconnectedCallback.apply(this); From ba87cea1b63e0c0e49d57e83ec76026e5ed26f32 Mon Sep 17 00:00:00 2001 From: Bowen Date: Mon, 23 Jun 2025 16:06:49 +0800 Subject: [PATCH 2/2] fix(i18n): extends causes memory leak --- projects/core/src/alert/alert.performance.ts | 2 +- projects/core/src/grid/grid.performance.ts | 2 +- .../core/src/internal/decorators/i18n.spec.ts | 27 ++++++++++++++++--- projects/core/src/internal/decorators/i18n.ts | 12 +++++---- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/projects/core/src/alert/alert.performance.ts b/projects/core/src/alert/alert.performance.ts index e936448a3..48f850f5d 100644 --- a/projects/core/src/alert/alert.performance.ts +++ b/projects/core/src/alert/alert.performance.ts @@ -16,7 +16,7 @@ describe('cds-badge performance', () => { `; it(`should bundle and treeshake alert`, async () => { - expect((await testBundleSize('@cds/core/alert/register.js')).kb).toBeLessThan(31.2); + expect((await testBundleSize('@cds/core/alert/register.js')).kb).toBeLessThan(31.3); }); it(`should render 1 alert under 20ms`, async () => { diff --git a/projects/core/src/grid/grid.performance.ts b/projects/core/src/grid/grid.performance.ts index 623026b45..2b0dc3a2d 100644 --- a/projects/core/src/grid/grid.performance.ts +++ b/projects/core/src/grid/grid.performance.ts @@ -11,7 +11,7 @@ import '@cds/core/grid/register.js'; describe('cds-grid bundle performance', () => { it(`should bundle and treeshake component`, async () => { const result = await testBundleSize(`import '@cds/core/grid/register.js'`); - expect(result.kb).toBeLessThan(37.5); + expect(result.kb).toBeLessThan(37.6); }); }); diff --git a/projects/core/src/internal/decorators/i18n.spec.ts b/projects/core/src/internal/decorators/i18n.spec.ts index deec5fd71..458f14263 100644 --- a/projects/core/src/internal/decorators/i18n.spec.ts +++ b/projects/core/src/internal/decorators/i18n.spec.ts @@ -43,6 +43,19 @@ class TestAlertI18nElement extends LitElement { } } +/** @element test-extends-18n-element */ +@customElement('test-extends-18n-element') +class TestExtendsI18nElement extends TestI18nElement { + @i18n() i18n = { + open: 'Open', + close: 'Close', + }; + + render() { + return html``; + } +} + describe('i18n decorator', () => { let testElement: HTMLElement; let component: TestI18nElement; @@ -191,12 +204,20 @@ describe('helpers', () => { }); }); -describe('unsubscribe', () => { - it('should unsubscribe from its subscription when the element is removed', async () => { +describe('subscription', () => { + it('should be unsubscribed when the element is removed', async () => { const testElement = await createTestElement( - html` ` + html` ` ); removeTestElement(testElement); expect((GlobalStateService.stateUpdates as any).subscriptions.length).toBe(0); }); + + it('should be unsubscribed when the element extends an @i8n element and is removed', async () => { + const testElement = await createTestElement(html` `); + const component = testElement.querySelector('test-extends-18n-element'); + expect(component.i18n).toEqual({ open: 'Open', close: 'Close' }); + removeTestElement(testElement); + expect((GlobalStateService.stateUpdates as any).subscriptions.length).toBe(0); + }); }); diff --git a/projects/core/src/internal/decorators/i18n.ts b/projects/core/src/internal/decorators/i18n.ts index 96b350bfe..586b082c6 100644 --- a/projects/core/src/internal/decorators/i18n.ts +++ b/projects/core/src/internal/decorators/i18n.ts @@ -58,11 +58,13 @@ export function i18n() { const targetDisconnectedCallback: () => void = protoOrDescriptor.disconnectedCallback; function connectedCallback(this: any): void { - this.__i18nSub = GlobalStateService.stateUpdates.subscribe(update => { - if (update.key === 'i18nRegistry') { - this.requestUpdate(name); - } - }); + if (!this.__i18nSub) { + this.__i18nSub = GlobalStateService.stateUpdates.subscribe(update => { + if (update.key === 'i18nRegistry') { + this.requestUpdate(name); + } + }); + } if (targetConnectedCallback) { targetConnectedCallback.apply(this);