Skip to content

Re-rendering performance improvements#1777

Open
Oksamies wants to merge 1 commit into04-02-refactor_replace_await_and_suspense_with_suspenseifpromise_globallyfrom
03-24-re-rendering_performance_improvements
Open

Re-rendering performance improvements#1777
Oksamies wants to merge 1 commit into04-02-refactor_replace_await_and_suspense_with_suspenseifpromise_globallyfrom
03-24-re-rendering_performance_improvements

Conversation

@Oksamies
Copy link
Copy Markdown
Contributor

No description provided.

Copy link
Copy Markdown
Contributor Author

Oksamies commented Mar 24, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 24, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2c9a4f25-509f-4896-9cac-255a6e4ee982

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request refactors four components to improve rendering efficiency. It extracts inline JSX into new memoized child components: OtherFilters in PackageSearch, AdsColumn in root.tsx, and wraps existing components AdContainer and CardPackage with React.memo. The OtherFilters component consolidates checkbox state handling using useMemo and useCallback hooks. These changes reduce re-render scope by isolating components that don't depend on parent-level state changes.

Possibly related PRs

  • Fix PackageSearch issues #1702: Modifies PackageSearch checkbox item value handling and state mapping logic, directly relevant to the checkbox state refactoring in OtherFilters.
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided, but the title and changes are self-explanatory and directly related to performance optimization through component memoization. Consider adding a brief description explaining the performance benefit or which re-rendering issues are being addressed.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the changeset—converting components to memoized versions and extracting sub-components to reduce unnecessary re-renders.

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


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.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 0% with 88 lines in your changes missing coverage. Please review.
✅ Project coverage is 14.51%. Comparing base (5a52581) to head (ab7ef65).

Files with missing lines Patch % Lines
...p/commonComponents/PackageSearch/PackageSearch.tsx 0.00% 61 Missing ⚠️
apps/cyberstorm-remix/app/root.tsx 0.00% 21 Missing ⚠️
...torm/src/newComponents/AdContainer/AdContainer.tsx 0.00% 3 Missing ⚠️
...src/newComponents/Card/CardPackage/CardPackage.tsx 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1777      +/-   ##
==========================================
- Coverage   14.53%   14.51%   -0.02%     
==========================================
  Files         334      334              
  Lines       27394    27428      +34     
  Branches      854      854              
==========================================
  Hits         3981     3981              
- Misses      23413    23447      +34     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx (1)

400-418: ⚠️ Potential issue | 🟠 Major

Inline callback defeats CardPackage memoization.

Since CardPackage is now wrapped with memo(), the inline arrow function for packageLikeAction (lines 406-416) causes unnecessary re-renders on every parent update. Consider memoizing the like action callback.

Proposed fix using useCallback
+ const handlePackageLike = useCallback(
+   (namespace: string, name: string) => {
+     if (likeAction) {
+       likeAction(
+         ratedPackages.includes(`${namespace}-${name}`),
+         namespace,
+         name,
+         Boolean(currentUser?.username)
+       );
+     }
+   },
+   [likeAction, ratedPackages, currentUser?.username]
+ );

Then at the call site:

  <CardPackage
    key={`${p.namespace}-${p.name}`}
    packageData={p}
    isLiked={ratedPackages.includes(`${p.namespace}-${p.name}`)}
-   packageLikeAction={() => {
-     if (likeAction) {
-       likeAction(
-         ratedPackages.includes(`${p.namespace}-${p.name}`),
-         p.namespace,
-         p.name,
-         Boolean(currentUser?.username)
-       );
-     }
-   }}
+   packageLikeAction={() => handlePackageLike(p.namespace, p.name)}
  />

Note: Even this still creates an inline arrow, but it's much lighter. For full optimization, consider changing packageLikeAction signature to accept namespace and name params, allowing you to pass handlePackageLike directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx`
around lines 400 - 418, The inline arrow for packageLikeAction causes
CardPackage memo to break; fix by creating a memoized handler using useCallback
(e.g., handlePackageLike = useCallback((namespace, name) => { const isLiked =
ratedPackages.includes(`${namespace}-${name}`); if (likeAction)
likeAction(isLiked, namespace, name, Boolean(currentUser?.username)); },
[likeAction, ratedPackages, currentUser?.username])) and then pass
packageLikeAction={handlePackageLike} to CardPackage (and update CardPackage to
call packageLikeAction(namespace, name) if necessary) so no inline callbacks are
created on each render.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cyberstorm/src/newComponents/Card/CardPackage/CardPackage.tsx`:
- Line 46: The prop passed to CardPackage from PackageSearch.tsx
(packageLikeAction) is created inline inside a map, causing a new function
reference each render and defeating memo(); wrap the callback in
React.useCallback in PackageSearch.tsx (import useCallback if missing) to return
a stable function reference and pass that to CardPackage instead of an inline
arrow, and ensure the useCallback dependency array includes only the minimal
reactive values used by the callback so the reference stays stable when
appropriate.

---

Outside diff comments:
In `@apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx`:
- Around line 400-418: The inline arrow for packageLikeAction causes CardPackage
memo to break; fix by creating a memoized handler using useCallback (e.g.,
handlePackageLike = useCallback((namespace, name) => { const isLiked =
ratedPackages.includes(`${namespace}-${name}`); if (likeAction)
likeAction(isLiked, namespace, name, Boolean(currentUser?.username)); },
[likeAction, ratedPackages, currentUser?.username])) and then pass
packageLikeAction={handlePackageLike} to CardPackage (and update CardPackage to
call packageLikeAction(namespace, name) if necessary) so no inline callbacks are
created on each render.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5cd769e8-cf79-4747-8a16-0770fc39d124

📥 Commits

Reviewing files that changed from the base of the PR and between 5a52581 and ab7ef65.

📒 Files selected for processing (4)
  • apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx
  • apps/cyberstorm-remix/app/root.tsx
  • packages/cyberstorm/src/newComponents/AdContainer/AdContainer.tsx
  • packages/cyberstorm/src/newComponents/Card/CardPackage/CardPackage.tsx

}

export function CardPackage(props: Props) {
export const CardPackage = memo(function CardPackage(props: Props) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify all usages of CardPackage to check for inline function props
rg -n -A 10 '<CardPackage' --type ts --type tsx -g '!*.test.*' -g '!*.spec.*'

Repository: thunderstore-io/thunderstore-ui

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Search for CardPackage component usage (corrected file type flags)
rg -n -A 10 '<CardPackage' --type ts --type jsx

Repository: thunderstore-io/thunderstore-ui

Length of output: 102


🏁 Script executed:

#!/bin/bash
# Also check CardPackage.tsx file directly to confirm memo wrapper exists
wc -l packages/cyberstorm/src/newComponents/Card/CardPackage/CardPackage.tsx

Repository: thunderstore-io/thunderstore-ui

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Check the actual CardPackage.tsx around line 46
head -n 50 packages/cyberstorm/src/newComponents/Card/CardPackage/CardPackage.tsx | tail -n 15

Repository: thunderstore-io/thunderstore-ui

Length of output: 438


🏁 Script executed:

#!/bin/bash
# Search for CardPackage usage without file type flags
rg '<CardPackage' -A 10 --max-count 20

Repository: thunderstore-io/thunderstore-ui

Length of output: 2425


🏁 Script executed:

#!/bin/bash
# Check PackageSearch.tsx specifically around lines mentioned in review (406-416)
head -n 420 packages/cyberstorm/src/newComponents/PackageSearch/PackageSearch.tsx | tail -n 30

Repository: thunderstore-io/thunderstore-ui

Length of output: 203


🏁 Script executed:

#!/bin/bash
# Get full context of the CardPackage usage in PackageSearch
rg -B 5 -A 20 'packageLikeAction=.*={() =>' apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx

Repository: thunderstore-io/thunderstore-ui

Length of output: 206


🏁 Script executed:

#!/bin/bash
# Check if useCallback is used elsewhere in PackageSearch
rg 'useCallback' apps/cyberstorm-remix/app/commonComponents/PackageSearch/PackageSearch.tsx

Repository: thunderstore-io/thunderstore-ui

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Search for all CardPackage usages to see if there are other call sites
rg -B 3 -A 15 '<CardPackage' --max-count 50

Repository: thunderstore-io/thunderstore-ui

Length of output: 4070


Inline callback defeats memoization effectiveness.

The memo() wrapper is correctly applied, but in PackageSearch.tsx, packageLikeAction is passed as an inline arrow function inside the map loop. This creates a new reference on every render, causing CardPackage to re-render despite memoization.

Since the file already uses useCallback for other callbacks, wrap packageLikeAction in useCallback to stabilize the prop reference and realize the performance benefit of memoization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cyberstorm/src/newComponents/Card/CardPackage/CardPackage.tsx` at
line 46, The prop passed to CardPackage from PackageSearch.tsx
(packageLikeAction) is created inline inside a map, causing a new function
reference each render and defeating memo(); wrap the callback in
React.useCallback in PackageSearch.tsx (import useCallback if missing) to return
a stable function reference and pass that to CardPackage instead of an inline
arrow, and ensure the useCallback dependency array includes only the minimal
reactive values used by the callback so the reference stays stable when
appropriate.

@Oksamies Oksamies changed the base branch from master to graphite-base/1777 April 2, 2026 18:34
Copilot AI review requested due to automatic review settings April 2, 2026 18:34
@Oksamies Oksamies force-pushed the 03-24-re-rendering_performance_improvements branch from ab7ef65 to 52e5f88 Compare April 2, 2026 18:34
@Oksamies Oksamies changed the base branch from graphite-base/1777 to 04-02-refactor_replace_await_and_suspense_with_suspenseifpromise_globally April 2, 2026 18:34
throw new Response("Community not found", { status: 404 });
}

let cachedFilters: any = null;

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.

let cachedFilters: any = null;
let cachedCommunityId: string | null = null;
let cachedSeo: any = null;

Check failure

Code scanning / ESLint

Disallow the `any` type Error

Unexpected any. Specify a different type.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR targets React re-rendering performance in the Cyberstorm UI and the cyberstorm-remix app by memoizing frequently-rendered components/blocks and reducing unnecessary recomputation during renders/hydration.

Changes:

  • Wrap CardPackage and AdContainer with React.memo to reduce re-renders.
  • Memoize expensive/stable JSX blocks and outlet context in app/root.tsx, and extract the ads column into a memoized component.
  • Add a new search-param blob setter helper and introduce simple client-side caching for PackageSearch server-derived data; extend PackageOrder to accept an optional id.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/cyberstorm/src/newComponents/Card/CardPackage/CardPackage.tsx Memoize CardPackage component to reduce re-render frequency.
packages/cyberstorm/src/newComponents/AdContainer/AdContainer.tsx Memoize AdContainer component to reduce re-render frequency.
apps/cyberstorm-remix/cyberstorm/utils/searchParamsUtils.ts Introduce a new setter-based setParamsBlobValue helper and keep a legacy wrapper.
apps/cyberstorm-remix/app/root.tsx Memoize navigation JSX and outlet context; extract ads column into AdsColumn; tweak tooltip delay behavior.
apps/cyberstorm-remix/app/commonComponents/PackageSearch/components/PackageOrder.tsx Add optional id prop to allow customizing/selecting different instances.
apps/cyberstorm-remix/app/c/tabs/PackageSearch/PackageSearch.tsx Cache filters/seo between client navigations to avoid repeated serverLoader() calls.
apps/cyberstorm-remix/app/c/community.tsx Memoize header JSX and outlet context to reduce re-renders in nested routes.

Comment on lines 1 to 15
export function setParamsBlobValue<
SearchParamsType,
K extends keyof SearchParamsType,
>(setter: React.Dispatch<React.SetStateAction<SearchParamsType>>, key: K) {
return (v: SearchParamsType[K]) =>
setter((prevBlob) => ({ ...prevBlob, [key]: v }));
}

// Keep the old signature wrapper with `oldBlob` around for backward compatibility,
// although it should be refactored eventually.
export function setParamsBlobValueLegacy<
SearchParamsType,
K extends keyof SearchParamsType,
>(setter: (v: SearchParamsType) => void, oldBlob: SearchParamsType, key: K) {
return (v: SearchParamsType[K]) => setter({ ...oldBlob, [key]: v });
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

setParamsBlobValue now takes (setter, key) and uses React.Dispatch/React.SetStateAction, but this file has no React type imports and existing call sites in the app currently call setParamsBlobValue(setter, oldBlob, key). As written this will fail typechecking/build; either restore the original signature under setParamsBlobValue (and export the new setter-based helper under a new name), or update all callers to use setParamsBlobValueLegacy and add the necessary react type imports.

Suggested change
export function setParamsBlobValue<
SearchParamsType,
K extends keyof SearchParamsType,
>(setter: React.Dispatch<React.SetStateAction<SearchParamsType>>, key: K) {
return (v: SearchParamsType[K]) =>
setter((prevBlob) => ({ ...prevBlob, [key]: v }));
}
// Keep the old signature wrapper with `oldBlob` around for backward compatibility,
// although it should be refactored eventually.
export function setParamsBlobValueLegacy<
SearchParamsType,
K extends keyof SearchParamsType,
>(setter: (v: SearchParamsType) => void, oldBlob: SearchParamsType, key: K) {
return (v: SearchParamsType[K]) => setter({ ...oldBlob, [key]: v });
import type { Dispatch, SetStateAction } from 'react';
export function setParamsBlobValue<
SearchParamsType,
K extends keyof SearchParamsType,
>(setter: (v: SearchParamsType) => void, oldBlob: SearchParamsType, key: K) {
return (v: SearchParamsType[K]) => setter({ ...oldBlob, [key]: v });
}
export function setParamsBlobValueFromStateSetter<
SearchParamsType,
K extends keyof SearchParamsType,
>(setter: Dispatch<SetStateAction<SearchParamsType>>, key: K) {
return (v: SearchParamsType[K]) =>
setter((prevBlob) => ({ ...prevBlob, [key]: v }));

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +69
let cachedFilters: any = null;
let cachedCommunityId: string | null = null;
let cachedSeo: any = null;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

Module-scope caches are typed as any, which defeats type-safety and will likely trip @typescript-eslint/no-explicit-any (enabled via plugin:@typescript-eslint/recommended). Prefer typing these as the concrete loader return types (e.g. CommunityFilters | null and ReturnType<typeof createSeo> | null) so downstream code can rely on filters.sections/etc without implicit any.

Suggested change
let cachedFilters: any = null;
let cachedCommunityId: string | null = null;
let cachedSeo: any = null;
let cachedFilters: Awaited<
ReturnType<DapperTs["getCommunityFilters"]>
> | null = null;
let cachedCommunityId: string | null = null;
let cachedSeo: ReturnType<typeof createSeo> | null = null;

Copilot uses AI. Check for mistakes.
Comment on lines +378 to +379
{adContainerIds.map((cid, k_i) => (
<AdContainer key={k_i} containerId={cid} />
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

key={k_i} uses the array index, which can cause unnecessary remounts if the list ever changes. Since adContainerIds is an array of stable IDs, prefer using key={cid} for stable reconciliation.

Suggested change
{adContainerIds.map((cid, k_i) => (
<AdContainer key={k_i} containerId={cid} />
{adContainerIds.map((cid) => (
<AdContainer key={cid} containerId={cid} />

Copilot uses AI. Check for mistakes.
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