Skip to content

perf(store): simplify string hash caching in ImmutableMap#7838

Merged
steveruizok merged 1 commit intoperformance-stack-1from
sr/perf-immutable-map-hash-cache
Feb 4, 2026
Merged

perf(store): simplify string hash caching in ImmutableMap#7838
steveruizok merged 1 commit intoperformance-stack-1from
sr/perf-immutable-map-hash-cache

Conversation

@steveruizok
Copy link
Copy Markdown
Collaborator

@steveruizok steveruizok commented Feb 4, 2026

Closes #7833. Previously, once the number of shapes grew to 256, we were creating new hashes for all shapes on every frame.

Summary

  • Cache all strings regardless of length (removed 16-char minimum threshold)
  • Remove cache eviction logic (no more 255 entry limit and reset)
  • Simplify to unbounded cache since shape IDs are finite per document

The previous implementation avoided caching short strings and had an eviction policy. However, in tldraw's use case, the number of unique string keys is bounded by the document's shape/record count, so an unbounded cache is safe and reduces overhead.

Test plan

  • Typecheck passes
  • Manual testing with large documents

🤖 Generated with Claude Code


Note

Medium Risk
Changes the hashing hot-path used by ImmutableMap to always cache string hashes and removes cache eviction, which could increase memory use or alter performance characteristics on workloads with many unique strings.

Overview
Simplifies string hash caching in ImmutableMap by always routing string hashing through cachedHashString (removing the short-string threshold).

Removes the cache size/eviction logic and related constants, making stringHashCache an unbounded in-memory map that retains all hashed strings for the process lifetime.

Written by Cursor Bugbot for commit e3fefe0. This will update automatically on new commits. Configure here.

Remove cache size limits and always cache string hashes:

- Cache all strings regardless of length (removed 16-char minimum)
- Remove cache eviction logic (no more 255 entry limit)
- Simplify to unbounded cache since shape IDs are finite per document

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples Ready Ready Preview Feb 4, 2026 5:24pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics Ignored Ignored Feb 4, 2026 5:24pm
chat-template Ignored Ignored Feb 4, 2026 5:24pm
tldraw-docs Ignored Ignored Feb 4, 2026 5:24pm
tldraw-shader Ignored Ignored Feb 4, 2026 5:24pm
workflow-template Ignored Ignored Feb 4, 2026 5:24pm

Request Review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

if (hashed === undefined) {
hashed = hashString(string)
if (STRING_HASH_CACHE_SIZE === STRING_HASH_CACHE_MAX_SIZE) {
STRING_HASH_CACHE_SIZE = 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cache returns inherited Object properties

High Severity

stringHashCache is a plain {} and cachedHashString reads via stringHashCache[string], so keys like toString/constructor can resolve to inherited properties instead of undefined. That makes hash() return non-numbers, which then get coerced in bitwise ops and can cause incorrect map behavior (missed lookups/collisions).

Additional Locations (2)

Fix in Cursor Fix in Web

let hashed = stringHashCache[string]
if (hashed === undefined) {
hashed = hashString(string)
if (STRING_HASH_CACHE_SIZE === STRING_HASH_CACHE_MAX_SIZE) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Prototype pollution via string hash cache

High Severity

Because stringHashCache is a normal object, caching attacker-controlled strings like __proto__ (now included due to caching all strings) can mutate the cache object’s prototype via the __proto__ setter. This can destabilize hashing and is a prototype-pollution vector if hash() is reachable with untrusted input.

Additional Locations (1)

Fix in Cursor Fix in Web

if (hashed === undefined) {
hashed = hashString(string)
if (STRING_HASH_CACHE_SIZE === STRING_HASH_CACHE_MAX_SIZE) {
STRING_HASH_CACHE_SIZE = 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unbounded global cache can leak memory

Medium Severity

Removing the eviction/reset logic makes stringHashCache grow for the entire process lifetime. If hash() sees many distinct strings over time (multiple documents/sessions, transient IDs, or any non-shape keys), the cache retains them indefinitely and can create a memory leak in long-running apps.

Additional Locations (1)

Fix in Cursor Fix in Web

@steveruizok steveruizok changed the base branch from main to performance-stack-1 February 4, 2026 17:50
@steveruizok steveruizok merged commit 7702833 into performance-stack-1 Feb 4, 2026
18 of 19 checks passed
@steveruizok steveruizok deleted the sr/perf-immutable-map-hash-cache branch February 4, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ImmutableMap hashes short string keys on every lookup

1 participant