Skip to content

feat: optimize cache key generation for better performance#366

Closed
divineniiquaye wants to merge 2 commits intouni-stack:mainfrom
divineniiquaye:cache-performace
Closed

feat: optimize cache key generation for better performance#366
divineniiquaye wants to merge 2 commits intouni-stack:mainfrom
divineniiquaye:cache-performace

Conversation

@divineniiquaye
Copy link
Copy Markdown
Contributor

@divineniiquaye divineniiquaye commented Feb 5, 2026

I tested this new cache system using bitwise against uniwind 1.3.0 and saw a 1.17× difference in performance, @Brentlok kindly review and check if it's worth merging

Summary by CodeRabbit

  • Refactor
    • Improved internal style caching and key generation to reduce collisions and improve performance.
  • Bug Fix
    • Styles that include data attributes are no longer reused from cache, ensuring correct recomputation and more reliable styling results.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Added a boolean hasDataAttributes to StylesResult and propagated it via emptyState and resolveStyles; cache key generation now uses className:stateFlags (bit flags). Cache lookup uses cache.get(cacheKey) ?? resolveStyles(...), and results with data attributes are not cached.

Changes

Cohort / File(s) Summary
Native store updates
packages/uniwind/src/core/native/store.ts
Added hasDataAttributes: boolean to StylesResult and emptyState. Replaced string-based cache key with className:stateFlags using bit flags for isDisabled/isFocused/isPressed. Cache retrieval changed to cache.get(cacheKey) ?? this.resolveStyles(...). Results where hasDataAttributes === true are not cached; resolveStyles now returns hasDataAttributes.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 I count the bits, I hop and play,
className:flags guide my way,
when data-attributes flutter by,
I skip the cache and give a sigh,
nibble keys, then bound away.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: optimizing cache key generation using bit-flag operations for performance improvement, which aligns with the core modifications to the caching strategy in the store.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/uniwind/src/core/native/store.ts`:
- Around line 31-35: The cache key collapses undefined and false because
stateFlags only encodes truthy bits; update the cache key generation so
tri-state is preserved (e.g., add presence bits for each prop or include a
normalized representation of state) — change the logic around
stateFlags/cacheKey in store.ts (symbols: stateFlags, cacheKey, this.cache.get)
to include separate bits or a distinct marker for "defined vs undefined" for
isDisabled/isFocused/isPressed (or use a deterministic serialization of state)
so a cached result computed when state is omitted is not reused for an explicit
false (and vice versa).

@jpudysz
Copy link
Copy Markdown
Collaborator

jpudysz commented Feb 9, 2026

Hey @divineniiquaye I've tested it couple of times and I can see small improvement in our benchmark.

Before: 81-83 ms

Simulator Screenshot - iPhone 17 Pro Max - 2026-02-09 at 12 21 06

After: 80-83 ms

simulator_screenshot_3DC729DE-59E8-45B2-AD91-1E9EE046AAB2

Not sure about the readability though. It looks a little bit worse and harder to maintain

@divineniiquaye
Copy link
Copy Markdown
Contributor Author

@jpudysz I've updated the code to be readable, added missing hasDataAttributes field to StylesResult and initialize it in emptyState.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/uniwind/src/core/native/store.ts (1)

38-48: ⚠️ Potential issue | 🔴 Critical

Listener subscriptions accumulate on every cache hit — performance and memory regression.

With the ?? pattern, when the cache already contains the result, execution still falls through to lines 41-48. Every call to getStyles for the same cacheKey adds another UniwindListener.subscribe(…, { once: true }) closure. Over many renders this accumulates unbounded listeners, each holding a reference to cacheKey and result.dependencies. This is counterproductive for a performance-focused PR.

Guard the subscription so it only runs on a cache miss:

Proposed fix
-        const result = this.cache.get(cacheKey) ?? this.resolveStyles(className, componentProps, state)
+        const cached = this.cache.get(cacheKey)
+
+        if (cached !== undefined) {
+            return cached
+        }
+
+        const result = this.resolveStyles(className, componentProps, state)

         // Don't cache styles that depend on data attributes
         if (!result.hasDataAttributes) {

@Brentlok
Copy link
Copy Markdown
Contributor

Thanks for the PR.

As @jpudysz mentioned, the trade-off here doesn't align with our project goals. The performance gain is too small to justify the negative impact on readability and maintainability. Since the current code isn't causing any issues, we prefer to keep it as is.

As noted in our README, please reach out to us before starting work on refactors so we can align on the approach first. I'm closing this for now.

@Brentlok Brentlok closed this Feb 13, 2026
@divineniiquaye
Copy link
Copy Markdown
Contributor Author

divineniiquaye commented Feb 13, 2026

Thanks for the PR.

As @jpudysz mentioned, the trade-off here doesn't align with our project goals. The performance gain is too small to justify the negative impact on readability and maintainability. Since the current code isn't causing any issues, we prefer to keep it as is.

As noted in our README, please reach out to us before starting work on refactors so we can align on the approach first. I'm closing this for now.

Interesting 🤔, but okay. I found more optimizations improvements was giving a shot to see the outcome.

This was quite expected, anyways thanks for reviewing it.

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.

3 participants