Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/hungry-meals-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@hashicorp/design-system-components": patch
---

<!-- START components/tag -->
`Tag` - Fixed a performance issue when many tags are present on a page caused by the ResizeObserver
<!-- END -->

Dependencies - Added `tracked-built-ins`
1 change: 1 addition & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"luxon": "^3.4.2",
"prismjs": "^1.30.0",
"sass": "^1.83.0",
"tracked-built-ins": "^4.0.0",
"tabbable": "^6.2.0",
"tippy.js": "^6.3.7"
},
Expand Down
48 changes: 32 additions & 16 deletions packages/components/src/components/hds/tag/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { TrackedWeakSet } from 'tracked-built-ins';
import { assert } from '@ember/debug';
import { modifier } from 'ember-modifier';

Expand Down Expand Up @@ -33,23 +34,42 @@ export interface HdsTagSignature {
Element: HTMLSpanElement;
}

const overflowed = new TrackedWeakSet<Element>();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the key to getting this working - externalizing the observer is only possible if we can track what elements are overflowed. To do this with ember's tracking, tracked-built-ins provides a tracking-entangled WeakSet. This means that getters and other computed properties that rely on this WeakSet will recompute when items are added or removed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the performance times for before and after and this fix correct the issues greatly. Will add some screenshots to the PR description for visibility.


const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const textContainer = entry.target.querySelector(
'.hds-tag__text-container'
);
if (
textContainer &&
textContainer.scrollHeight > textContainer.clientHeight
) {
overflowed.add(entry.target);
} else {
overflowed.delete(entry.target);
}
});
});

export default class HdsTag extends Component<HdsTagSignature> {
@tracked private _isTextOverflow!: boolean;
private _observer!: ResizeObserver;
@tracked private _element?: HTMLElement;
private get _isTextOverflow(): boolean {
if (!this._element) {
return false;
}
return overflowed.has(this._element);
}

private _setUpObserver = modifier((element: HTMLElement) => {
// Used to detect when text is clipped to one line, and tooltip should be added
this._observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
this._isTextOverflow = this._isOverflow(
entry.target.querySelector('.hds-tag__text-container')!
);
});
});
this._observer.observe(element);
this._element = element;
observer.observe(element);

return () => {
this._observer.disconnect();
if (this._element) {
observer.unobserve(this._element);
}
delete this._element;
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using delete operator on object properties can impact performance and prevent optimizations. Consider setting this._element = undefined instead.

Suggested change
delete this._element;
this._element = undefined;

Copilot uses AI. Check for mistakes.

};
});

Expand Down Expand Up @@ -150,8 +170,4 @@ export default class HdsTag extends Component<HdsTagSignature> {

return classes.join(' ');
}

private _isOverflow(el: Element): boolean {
return el.scrollHeight > el.clientHeight;
}
}
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion showcase/app/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ Router.map(function () {
});
});
this.route('table');
this.route('tag');
this.route('tag', function () {
this.route('frameless', function () {
this.route('demo-observer-performance');
});
});
this.route('text');
this.route('time');
this.route('toast');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/

import Route from '@ember/routing/route';

export default class PageComponentsTagFramelessDemoObserverPerformanceRoute extends Route {
model() {
const DEMO_RANGE = Array(1000)
.fill(1)
.map((n, i) => ({ index: n + i }));
return { DEMO_RANGE };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}

{{page-title "Tag - Observer performance demo - Frameless"}}

<div {{style padding="24px"}}>
<Shw::Text::Body><strong>Note:</strong>
This demo is to test the loading performance of the ResizeObserver when many Tags are rendered. There should be
little increase in loading times even when many Tags with observers are present.</Shw::Text::Body>

<Hds::Layout::Grid @columnWidth="150px" @gap="16">
{{#each @model.DEMO_RANGE as |i|}}
<Hds::Tag @text="{{i.index}} This is a very long text that should go on multiple lines" />
{{/each}}
</Hds::Layout::Grid>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,20 @@
</SF.Item>
</Shw::Flex>

<Shw::Divider @level={{2}} />

</section>

{{! For some reason, Ember tests don't play well with iframes (URL not found) so we can't take snapshots of these examples in Percy }}
<section>

<Shw::Text::H2>Demos</Shw::Text::H2>

<Shw::Frame
@id="demo-tag-performance"
@src="/components/tag/frameless/demo-observer-performance"
@height="300"
@label="Performance test for ResizeObserver"
/>

</section>