Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cdb5db8
Replace isDefined() with inline !== undefined in transform
titouanmathis Apr 2, 2026
f627635
Replace isDefined() with inline !== undefined in animate and tween
titouanmathis Apr 2, 2026
051c8f5
Use incremental sum for mean progress calculation in animate
titouanmathis Apr 2, 2026
7fd6d4e
Replace delegate pattern with direct functions in animate
titouanmathis Apr 2, 2026
893c2e8
Add fast path for numeric translate values in animate
titouanmathis Apr 2, 2026
f0b169a
Build transform string directly in render to avoid object allocation
titouanmathis Apr 2, 2026
b86c67b
Pre-compute keyframe deltas at creation time using staged compilation
titouanmathis Apr 2, 2026
4704b1a
Batch scheduler calls in RafService.trigger to reduce closure creation
titouanmathis Apr 2, 2026
001fbbe
Cache scroll max values in ScrollService and update on resize only
titouanmathis Apr 2, 2026
d52459f
Replace array spread with push in getAllProperties prototype walk
titouanmathis Apr 2, 2026
87ff506
Return storage Set directly from getInstances instead of copying
titouanmathis Apr 2, 2026
da84702
Update changelog with performance improvements
titouanmathis Apr 3, 2026
104eee2
Restore defensive copy in getInstances
titouanmathis Apr 11, 2026
3f1e449
Use maximum duration for multi-element animate progress
titouanmathis Apr 11, 2026
559e692
Add regression tests for compiled animate segments
titouanmathis Apr 11, 2026
d7352b2
Observe scroll size changes in ScrollService
titouanmathis Apr 11, 2026
b8b6cc8
Fix ScrollService resize and cache invalidation
titouanmathis Apr 11, 2026
d16c334
Handle missing custom property keyframes in animate
titouanmathis Apr 11, 2026
9a15506
Keep transform origin reads in animate read phase
titouanmathis Apr 12, 2026
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ All notable changes to this project will be documented in this file. The format

- Add browser storage utilities: `createStorage`, `useLocalStorage`, `useSessionStorage`, `useUrlSearchParams`, `useUrlSearchParamsInHash` ([#671](https://github.com/studiometa/js-toolkit/pull/671))
- Add `logTree` helper to inspect the component tree from the console ([#654](https://github.com/studiometa/js-toolkit/pull/654))
- Add performance benchmarks for tween, animate, transform, services and Base internals ([#722](https://github.com/studiometa/js-toolkit/pull/722))
- Add CodSpeed CI integration for continuous performance tracking ([#723](https://github.com/studiometa/js-toolkit/pull/723))

### Changed

- **Performance**
- Improve `animate` progress updates by pre-computing keyframe deltas at creation time ([#721](https://github.com/studiometa/js-toolkit/pull/721), [ca72182c](https://github.com/studiometa/js-toolkit/commit/ca72182c))
- Improve `RafService.trigger` by batching scheduler calls, reducing closure creation from 2N to 2 per cycle ([#721](https://github.com/studiometa/js-toolkit/pull/721), [d4f96331](https://github.com/studiometa/js-toolkit/commit/d4f96331))
- Improve `ScrollService.updateProps` by caching scroll max values and updating on resize only ([#721](https://github.com/studiometa/js-toolkit/pull/721), [d5939211](https://github.com/studiometa/js-toolkit/commit/d5939211))
- Improve `transform` and `tween` by replacing `isDefined()` with inline `!== undefined` checks ([#721](https://github.com/studiometa/js-toolkit/pull/721), [e580bfee](https://github.com/studiometa/js-toolkit/commit/e580bfee), [1fc79012](https://github.com/studiometa/js-toolkit/commit/1fc79012))
- Improve `getAllProperties` by replacing O(nΒ²) array spread with O(n) push ([#721](https://github.com/studiometa/js-toolkit/pull/721), [3dea1808](https://github.com/studiometa/js-toolkit/commit/3dea1808))
- Improve `getInstances` by returning storage Set directly instead of copying ([#721](https://github.com/studiometa/js-toolkit/pull/721), [221be81e](https://github.com/studiometa/js-toolkit/commit/221be81e))

### Fixed

Expand Down
8 changes: 4 additions & 4 deletions packages/js-toolkit/Base/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,13 @@ function getElementsStorage(): Set<HTMLElement & { __base__: Map<string, Base>}>
* Get all mounted instances or the ones from a given component.
* @link https://js-toolkit.studiometa.dev/api/helpers/getInstances.html
*/
export function getInstances(): Set<Base>;
export function getInstances(): ReadonlySet<Base>;
export function getInstances<T extends BaseConstructor = BaseConstructor>(
ctor: T,
): Set<InstanceType<T>>;
export function getInstances<T extends BaseConstructor = BaseConstructor>(
ctor?: T,
): Set<InstanceType<T>> | Set<Base> {
): ReadonlySet<InstanceType<T>> | ReadonlySet<Base> {
if (isDefined(ctor)) {
const filteredInstances = new Set<InstanceType<T>>();
for (const instance of getInstancesStorage()) {
Expand All @@ -144,9 +144,9 @@ export function getInstances<T extends BaseConstructor = BaseConstructor>(
}
}
return filteredInstances;
} else {
return new Set(getInstancesStorage());
}

return new Set(getInstancesStorage());
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

export function getElements() {
Expand Down
4 changes: 2 additions & 2 deletions packages/js-toolkit/helpers/queryComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function queryComponent<T extends Base = Base>(
): T | undefined {
const parsedQuery = parseQuery(query);
const { from = document } = options;
const instances = getInstances() as Set<T>;
const instances = getInstances() as ReadonlySet<T>;

for (const instance of instances) {
if (!instanceIsMatching(instance, parsedQuery)) continue;
Expand All @@ -74,7 +74,7 @@ export function queryComponentAll<T extends Base = Base>(
): T[] {
const parsedQuery = parseQuery(query);
const { from = document } = options;
const instances = getInstances() as Set<T>;
const instances = getInstances() as ReadonlySet<T>;
const selectedInstances = new Set<T>();

for (const instance of instances) {
Expand Down
25 changes: 16 additions & 9 deletions packages/js-toolkit/services/RafService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,24 @@ export class RafService extends AbstractService<RafServiceProps> {
};

trigger(props: RafServiceProps) {
for (const callback of this.callbacks.values()) {
this.scheduler.read(() => {
const render = callback(props) as (() => unknown) | void;
this.scheduler.read(() => {
const writeQueue: Array<(props: RafServiceProps) => unknown> = [];
Comment thread
titouanmathis marked this conversation as resolved.

for (const callback of this.callbacks.values()) {
const render = callback(props) as ((props: RafServiceProps) => unknown) | void;
if (isFunction(render)) {
this.scheduler.write(() => {
// @ts-ignore
render(props);
});
writeQueue.push(render);
}
});
}
}

if (writeQueue.length > 0) {
this.scheduler.write(() => {
for (let i = 0; i < writeQueue.length; i++) {
writeQueue[i](props);
}
});
}
});
}

loop() {
Expand Down
69 changes: 62 additions & 7 deletions packages/js-toolkit/services/ScrollService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,23 @@ export interface ScrollServiceProps {
export type ScrollServiceInterface = ServiceInterface<ScrollServiceProps>;

export class ScrollService extends AbstractService<ScrollServiceProps> {
static config: ServiceConfig = [[() => document, [['scroll', PASSIVE_CAPTURE_EVENT_OPTIONS]]]];
static config: ServiceConfig = [
[() => document, [['scroll', PASSIVE_CAPTURE_EVENT_OPTIONS]]],
[() => window, [['resize', PASSIVE_CAPTURE_EVENT_OPTIONS]]],
];

/**
* Cached scroll max values, updated on resize and first scroll.
* @private
*/
__maxX: number | null = null;
__maxY: number | null = null;

/**
* Observe scroll container size changes that do not trigger window resize.
* @private
*/
__resizeObserver: ResizeObserver | null = null;

props: ScrollServiceProps = {
x: window.scrollX,
Expand Down Expand Up @@ -90,8 +106,11 @@ export class ScrollService extends AbstractService<ScrollServiceProps> {
props.last.y = props.lastY = yLast;
props.delta.x = props.deltaX = props.x - xLast;
props.delta.y = props.deltaY = props.y - yLast;
props.max.x = props.maxX = document.scrollingElement.scrollWidth - window.innerWidth;
props.max.y = props.maxY = document.scrollingElement.scrollHeight - window.innerHeight;
if (this.__maxX === null || this.__maxY === null) {
this.__updateMaxValues();
}
props.max.x = props.maxX = this.__maxX;
props.max.y = props.maxY = this.__maxY;
props.progress.x = props.progressX = props.max.x === 0 ? 1 : props.x / props.max.x;
props.progress.y = props.progressY = props.max.y === 0 ? 1 : props.y / props.max.y;
props.isUp = props.y < yLast;
Expand All @@ -106,21 +125,57 @@ export class ScrollService extends AbstractService<ScrollServiceProps> {
return props;
}

update() {
nextTick(() => this.updateProps()).then(() => this.trigger(this.props));
update(trigger = true) {
nextTick(() => this.updateProps()).then(() => {
if (trigger) {
this.trigger(this.props);
}
});
}

onScrollDebounced = debounce(() => {
this.update();
}, 100);

/**
* Scroll handler.
* Update cached max scroll values on resize.
* @private
*/
__updateMaxValues() {
this.__maxX = document.scrollingElement.scrollWidth - window.innerWidth;
this.__maxY = document.scrollingElement.scrollHeight - window.innerHeight;
}

/**
* Scroll and resize handler.
*/
handleEvent() {
handleEvent(event: Event) {
if (event.type === 'resize') {
this.__updateMaxValues();
this.update(false);
return;
}

this.update();
this.onScrollDebounced();
}

init() {
super.init();

if (typeof ResizeObserver === 'function' && document.scrollingElement) {
this.__resizeObserver = new ResizeObserver(() => {
this.__updateMaxValues();
});
this.__resizeObserver.observe(document.scrollingElement);
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

kill() {
this.__resizeObserver?.disconnect();
this.__resizeObserver = null;
super.kill();
}
}

/**
Expand Down
Loading
Loading