perf(sticky): remove creation delay + add dirty-checking to StickySprite

- Replace 50ms setTimeout with queueMicrotask for editor open
- Skip updateFromData() entirely when no inputs changed
- Cache background redraw inputs, skip Graphics clear+redraw when shape unchanged
- Separate text-only, style-only, and bg-only update paths
This commit is contained in:
Hiren Kangad
2026-03-12 22:09:12 +05:30
parent 565b7c6c19
commit f12b75395a
2 changed files with 106 additions and 22 deletions
+103 -20
View File
@@ -10,6 +10,11 @@
* - width is fixed (from data.w) * - width is fixed (from data.w)
* - height auto-grows to fit wrapped text + padding * - height auto-grows to fit wrapped text + padding
* - parent SceneItem.data.h is updated after layout * - parent SceneItem.data.h is updated after layout
*
* Performance:
* - updateFromData() skips work when inputs haven't changed
* - _drawBg() is cached and only redraws when dimensions/color change
* - showText() never triggers layout or redraw
*/ */
import { Container, Graphics, Text, TextStyle } from 'pixi.js'; import { Container, Graphics, Text, TextStyle } from 'pixi.js';
@@ -32,6 +37,19 @@ export class StickySprite extends Container {
private _cornerRadius = DEFAULT_CORNER_RADIUS; private _cornerRadius = DEFAULT_CORNER_RADIUS;
private _bgColor = '#ffd43b'; private _bgColor = '#ffd43b';
// Dirty-check cache: last known inputs to avoid redundant work
private _lastText = '';
private _lastFontSize = 14;
private _lastFontFamily = 'Inter, system-ui, sans-serif';
private _lastTextColor = '#1a1a1a';
private _lastFill = '#ffd43b';
private _lastW = 200;
private _lastPadding = DEFAULT_PADDING;
private _lastCornerRadius = DEFAULT_CORNER_RADIUS;
// Background redraw cache: skip Graphics.clear()+redraw when shape is unchanged
private _bgCacheKey = '';
/** After layout, the computed card height. Parent should sync to data.h. */ /** After layout, the computed card height. Parent should sync to data.h. */
get computedHeight(): number { get computedHeight(): number {
return this._cardH; return this._cardH;
@@ -55,57 +73,122 @@ export class StickySprite extends Container {
this.addChild(this._bg); this.addChild(this._bg);
// Text style // Text style
const fontSize = data.fontSize || 14;
this._style = new TextStyle({ this._style = new TextStyle({
fontSize: data.fontSize || 14, fontSize,
fontFamily: data.fontFamily || 'Inter, system-ui, sans-serif', fontFamily: data.fontFamily || 'Inter, system-ui, sans-serif',
fill: data.textColor || '#1a1a1a', fill: data.textColor || '#1a1a1a',
wordWrap: true, wordWrap: true,
wordWrapWidth: Math.max(data.w - this._padding * 2, 1), wordWrapWidth: Math.max(data.w - this._padding * 2, 1),
lineHeight: (data.fontSize || 14) * DEFAULT_LINE_HEIGHT, lineHeight: fontSize * DEFAULT_LINE_HEIGHT,
}); });
this._text = new Text({ text: data.text || '', style: this._style }); this._text = new Text({ text: data.text || '', style: this._style });
this._text.position.set(this._padding, this._padding); this._text.position.set(this._padding, this._padding);
this.addChild(this._text); this.addChild(this._text);
// Snapshot initial state
this._lastText = data.text || '';
this._lastFontSize = fontSize;
this._lastFontFamily = data.fontFamily || 'Inter, system-ui, sans-serif';
this._lastTextColor = data.textColor || '#1a1a1a';
this._lastFill = this._bgColor;
this._lastW = data.w;
this._lastPadding = this._padding;
this._lastCornerRadius = this._cornerRadius;
this._layout(); this._layout();
} }
/** /**
* Update from StickyObject data. Called by SceneManager._updateItem(). * Update from StickyObject data. Called by SceneManager._updateItem().
* Skips text/style/layout work when nothing actually changed.
*/ */
updateFromData(data: StickyObject): void { updateFromData(data: StickyObject): void {
this._padding = data.padding ?? DEFAULT_PADDING; const padding = data.padding ?? DEFAULT_PADDING;
this._cornerRadius = data.cornerRadius ?? DEFAULT_CORNER_RADIUS; const cornerRadius = data.cornerRadius ?? DEFAULT_CORNER_RADIUS;
this._bgColor = data.fill || '#ffd43b'; const bgColor = data.fill || '#ffd43b';
this._cardW = data.w; const w = data.w;
const text = data.text || '';
const fontSize = data.fontSize || 14;
const fontFamily = data.fontFamily || 'Inter, system-ui, sans-serif';
const textColor = data.textColor || '#1a1a1a';
// Update text content and style // Check if anything actually changed
this._text.text = data.text || ''; const textChanged = text !== this._lastText;
this._style.fontSize = data.fontSize || 14; const styleChanged =
this._style.fontFamily = data.fontFamily || 'Inter, system-ui, sans-serif'; fontSize !== this._lastFontSize ||
this._style.fill = data.textColor || '#1a1a1a'; fontFamily !== this._lastFontFamily ||
this._style.wordWrapWidth = Math.max(data.w - this._padding * 2, 1); textColor !== this._lastTextColor;
this._style.lineHeight = (data.fontSize || 14) * DEFAULT_LINE_HEIGHT; const layoutChanged =
this._text.style = this._style; w !== this._lastW ||
padding !== this._lastPadding;
const bgChanged =
bgColor !== this._lastFill ||
cornerRadius !== this._lastCornerRadius;
this._text.position.set(this._padding, this._padding); // Nothing changed — skip all work
this._layout(); if (!textChanged && !styleChanged && !layoutChanged && !bgChanged) return;
// Update cached state
this._padding = padding;
this._cornerRadius = cornerRadius;
this._bgColor = bgColor;
this._cardW = w;
this._lastText = text;
this._lastFontSize = fontSize;
this._lastFontFamily = fontFamily;
this._lastTextColor = textColor;
this._lastFill = bgColor;
this._lastW = w;
this._lastPadding = padding;
this._lastCornerRadius = cornerRadius;
// Only update text if content changed
if (textChanged) {
this._text.text = text;
}
// Only update style if style/layout props changed
if (styleChanged || layoutChanged) {
this._style.fontSize = fontSize;
this._style.fontFamily = fontFamily;
this._style.fill = textColor;
this._style.wordWrapWidth = Math.max(w - padding * 2, 1);
this._style.lineHeight = fontSize * DEFAULT_LINE_HEIGHT;
this._text.style = this._style;
this._text.position.set(padding, padding);
}
// Relayout if text, style, or dimensions changed (need remeasure)
if (textChanged || styleChanged || layoutChanged) {
this._layout();
} else if (bgChanged) {
// Only bg color/radius changed — redraw background without remeasuring text
this._drawBg();
}
} }
/** /**
* Recompute layout: measure text height, resize card, redraw background. * Recompute layout: measure text height, resize card, redraw background.
* Returns the new card height so the caller can sync data.h.
*/ */
private _layout(): void { private _layout(): void {
// Measure the PixiJS text after style update
const textBounds = this._text.getLocalBounds(); const textBounds = this._text.getLocalBounds();
const textH = textBounds.height; const textH = textBounds.height;
// Card height: text + top/bottom padding, minimum height
this._cardH = Math.max(textH + this._padding * 2, MIN_HEIGHT); this._cardH = Math.max(textH + this._padding * 2, MIN_HEIGHT);
this._drawBg();
}
/**
* Redraw the rounded-rect background. Skips if shape is unchanged
* (same dimensions, color, radius).
*/
private _drawBg(): void {
const cacheKey = `${this._cardW}|${this._cardH}|${this._bgColor}|${this._cornerRadius}`;
if (cacheKey === this._bgCacheKey) return;
this._bgCacheKey = cacheKey;
// Redraw background
this._bg.clear(); this._bg.clear();
const color = parseInt(this._bgColor.replace('#', ''), 16) || 0xffd43b; const color = parseInt(this._bgColor.replace('#', ''), 16) || 0xffd43b;
this._bg.roundRect(0, 0, this._cardW, this._cardH, this._cornerRadius); this._bg.roundRect(0, 0, this._cardW, this._cardH, this._cornerRadius);
+3 -2
View File
@@ -348,7 +348,8 @@ export function activateTool(
const canvasEl = container.querySelector('canvas'); const canvasEl = container.querySelector('canvas');
const domContainer = canvasEl?.parentElement ?? container; const domContainer = canvasEl?.parentElement ?? container;
setTimeout(() => { // Use microtask instead of 50ms delay — PixiJS only needs one tick
queueMicrotask(() => {
ctx.textEditor!.startEditing(item, viewport, domContainer, () => { ctx.textEditor!.startEditing(item, viewport, domContainer, () => {
// Cleanup rule — fires on BOTH save and cancel (stopEditing always calls _onChange): // Cleanup rule — fires on BOTH save and cancel (stopEditing always calls _onChange):
// - new sticky + cancel/blur with empty text => remove (no blank notes left behind) // - new sticky + cancel/blur with empty text => remove (no blank notes left behind)
@@ -362,7 +363,7 @@ export function activateTool(
ctx.onChange(); ctx.onChange();
}); });
ctx.textEditor!.clearText(); ctx.textEditor!.clearText();
}, 50); });
} }
ctx.switchToSelect?.(); ctx.switchToSelect?.();