Skip to content

feat: optimize getSpeakerByYearAndId with O(1) Map lookup - #368

Open
anyulled wants to merge 1 commit into
mainfrom
feat/perf-get-speaker-map-337368392178284315
Open

feat: optimize getSpeakerByYearAndId with O(1) Map lookup#368
anyulled wants to merge 1 commit into
mainfrom
feat/perf-get-speaker-map-337368392178284315

Conversation

@anyulled

@anyulled anyulled commented Aug 3, 2026

Copy link
Copy Markdown
Owner

💡 What: Refactored getSpeakerByYearAndId to use a cached getSpeakersMap function that generates a Map<string, Speaker> instead of linearly searching the raw array using .find().

🎯 Why: In components or build-time functions that render or process lists of objects (like schedule sessions looking up multiple speakers), repeatedly calling .find() on the same large array leads to redundant O(N) iterations. Converting the array to a Map once (memoized per-request via React's cache) transforms these lookups into O(1) operations, drastically reducing CPU cycles during data-heavy rendering paths.

📊 Impact: Reduces time complexity of speaker lookups from O(N) to O(1) for subsequent lookups within the same React rendering lifecycle/request.

🔬 Measurement: Verified that tests (npm run test) continue to pass and hooks_performance.test.ts confirms data is still cached efficiently. The impact can be seen in reduced flamegraph time for components heavily iterating over sessions and resolving speaker IDs.


PR created automatically by Jules for task 337368392178284315 started by @anyulled

Summary by CodeRabbit

  • Performance Improvements
    • Improved speaker lookups for faster retrieval by ID.
    • Added performance guidance for efficient repeated searches.

Converts the linear O(N) array search inside getSpeakerByYearAndId to an O(1) Map lookup using a cached Map generation function.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 699ae09b-e562-48a8-854e-bd497a0d68ac

📥 Commits

Reviewing files that changed from the base of the PR and between 557a21c and 4a9ea30.

📒 Files selected for processing (2)
  • .jules/bolt.md
  • hooks/useSpeakers.ts

📝 Walkthrough

Walkthrough

The speaker hook now builds cached per-year Map instances and uses direct speaker ID lookup. The performance guideline documents this pattern for repeatedly searched arrays.

Changes

Speaker lookup optimization

Layer / File(s) Summary
Cached speaker map lookup
hooks/useSpeakers.ts, .jules/bolt.md
The hook caches speakers by year and ID. getSpeakerByYearAndId uses direct Map lookup. The guideline documents the cached Map pattern.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Poem

I mapped each speaker, ID by ID,
No .find() burrows left to hide.
The cache now hops with nimble feet,
Direct lookups make the path complete.
Squeak! A faster search for me.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing linear speaker searches with an O(1) Map lookup.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/perf-get-speaker-map-337368392178284315

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Optimize speaker lookup via cached Map in getSpeakerByYearAndId

✨ Enhancement 📝 Documentation 🕐 10-20 Minutes

Grey Divider

AI Description

• Add a cached speakers Map builder to avoid repeated linear scans.
• Switch getSpeakerByYearAndId from Array.find() to Map.get() for O(1) lookups.
• Document the O(1) Map lookup guideline for repeated ID-based searches.
Diagram

sequenceDiagram
  participant UI as Pages/Components
  participant GS as getSpeakerByYearAndId
  participant GSM as getSpeakersMap (React cache)
  participant GSA as getSpeakers (React cache)
  participant SZ as Sessionize Speakers API

  UI->>GS: (year, speakerId)
  GS->>GSM: get(year)
  GSM->>GSA: getSpeakers(year)
  GSA->>SZ: GET /view/Speakers
  SZ-->>GSA: Speaker[]
  GSA-->>GSM: Speaker[]
  GSM-->>GS: Map<id, Speaker>
  GS-->>UI: Map.get(speakerId)
Loading
High-Level Assessment

The cached Map approach is appropriate here: it preserves the existing API while eliminating repeated O(N) scans during data-heavy rendering paths. Considered alternatives include (a) pushing callers to fetch speakers once and pass a pre-built index around, or (b) using a plain object/Record for indexing; both add either wider call-site churn or less explicit key semantics than Map for this use case.

Files changed (2) +12 / -2

Enhancement (1) +7 / -2
useSpeakers.tsUse cached Map for getSpeakerByYearAndId lookups +7/-2

Use cached Map for getSpeakerByYearAndId lookups

• Introduces a cached getSpeakersMap(year) that converts the fetched Speaker[] into a Map keyed by speaker.id. Refactors getSpeakerByYearAndId to fetch the cached Map and use Map.get() instead of Array.find().

hooks/useSpeakers.ts

Documentation (1) +5 / -0
bolt.mdDocument Map-based O(1) lookup guidance +5/-0

Document Map-based O(1) lookup guidance

• Adds an internal learning note recommending converting repeated array ID lookups into a cached Map for O(1) access. Captures the rationale and the suggested Map construction pattern.

.jules/bolt.md

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 18 invoked
  gsap-timeline
  gsap-performance
  gsap-react
  gsap-frameworks
  gsap-utils
  next-cache-components
  gsap-core
  seo
  gsap-scrolltrigger
  gsap-plugins
  vercel-composition-patterns
  typescript-advanced-types
  nodejs-best-practices
  nodejs-backend-patterns
  accessibility
  supabase-postgres-best-practices
  next-best-practices
  vercel-react-best-practices

Grey Divider


Remediation recommended

1. React cache mock collisions 🐞 Bug ☼ Reliability
Description
With this PR, hooks/useSpeakers.ts now has two React.cache-wrapped functions that can be called with
the same single argument (year), but the Jest mock for react.cache uses one shared cache keyed only
by JSON.stringify(args). This can cause cached values for getSpeakers(year) and getSpeakersMap(year)
to overwrite each other (wrong return types / order-dependent tests) under the mock implementation.
Code

hooks/useSpeakers.ts[R38-41]

+const getSpeakersMap = cache(async (year: string | number): Promise<Map<string, Speaker>> => {
  const speakers = await getSpeakers(year);
-  return speakers.find((speaker) => speaker.id === speakerId);
+  return new Map(speakers.map((speaker) => [speaker.id, speaker]));
+});
Relevance

●●● Strong

PR #10 precedent: cache mock should use per-function map to avoid leakage/order-dependence;
shared-map collisions likely fixed.

PR-#10

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The performance-test mock keys the cache only by args and shares one Map across all cached
functions; the PR introduces a second cache-wrapped function in the same module that commonly
receives the same args (year), enabling collisions under that mock.

hooks/useSpeakers.ts[14-46]
tests/hooks_performance.test.ts[17-35]
tests/schedule_performance.test.ts[13-31]
PR-#10

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The Jest mock for `react.cache` in performance tests uses a single shared `cacheMap` keyed only by `JSON.stringify(args)`. After this PR adds `getSpeakersMap(year)` (also `cache`-wrapped) which calls `getSpeakers(year)`, the two cached functions can collide under the same key (e.g. `['2024']`), causing test-only incorrect behavior and potential flakiness.

### Issue Context
Production React `cache` scopes memoization per cached function; the current test mock does not, so adding a second cached function in the same module creates new collision opportunities.

### Fix Focus Areas
- __tests__/hooks_performance.test.ts[17-35]
- __tests__/schedule_performance.test.ts[13-31]

### Suggested fix
Update the mock to maintain a separate `Map` per wrapped function (e.g., create `const cacheMap = new Map()` inside the `cache: (fn) => { ... }` closure, or use a `WeakMap<Function, Map>` keyed by `fn`). If you need to clear caches between tests, track per-function maps and clear them in `beforeEach`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Duplicate IDs change winner 🐞 Bug ≡ Correctness
Description
getSpeakerByYearAndId now builds a Map from the speakers array, which overwrites earlier entries for
duplicate speaker.id values; the previous .find() implementation returned the first match. If the
fetched list contains duplicate IDs, the returned Speaker for the same (year, speakerId) will
change.
Code

hooks/useSpeakers.ts[R39-41]

  const speakers = await getSpeakers(year);
-  return speakers.find((speaker) => speaker.id === speakerId);
+  return new Map(speakers.map((speaker) => [speaker.id, speaker]));
+});
Relevance

●● Moderate

Duplicate-id overwrite vs first-match is a plausible edge case, but no historical pattern requiring
preserving .find() semantics.

PR-#11

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new code constructs a Map from [speaker.id, speaker] pairs, and Map semantics overwrite
earlier keys; there is no in-repo uniqueness enforcement for Speaker.id, so duplicates would
change which record is returned.

hooks/useSpeakers.ts[38-46]
hooks/types.ts[12-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`new Map(speakers.map(([id, speaker])))` keeps the *last* occurrence for a duplicate `speaker.id`, while the prior `.find()` returned the *first* occurrence. This is a subtle semantic change for duplicate IDs.

### Issue Context
Speaker ID uniqueness is not enforced in-repo (types only declare `id: string`). Defensive behavior should ideally remain stable.

### Fix Focus Areas
- hooks/useSpeakers.ts[38-41]

### Suggested fix
Build the map with a loop that preserves the first occurrence:
```ts
const map = new Map<string, Speaker>();
for (const speaker of speakers) {
 if (!map.has(speaker.id)) map.set(speaker.id, speaker);
}
return map;
```
Optionally, detect duplicates and `console.warn` once per year to surface upstream data issues.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Map build removes early-exit 🐞 Bug ➹ Performance
Description
getSpeakerByYearAndId now always constructs a full Map of all speakers for the year before doing a
single lookup, eliminating the early-exit behavior of the previous .find() for one-off calls. This
can add unnecessary CPU/memory on call sites that only need one speaker (e.g., OpenGraph image
generation).
Code

hooks/useSpeakers.ts[R38-45]

+const getSpeakersMap = cache(async (year: string | number): Promise<Map<string, Speaker>> => {
  const speakers = await getSpeakers(year);
-  return speakers.find((speaker) => speaker.id === speakerId);
+  return new Map(speakers.map((speaker) => [speaker.id, speaker]));
+});
+
+export const getSpeakerByYearAndId = async (year: string | number, speakerId: string): Promise<Speaker | undefined> => {
+  const speakersMap = await getSpeakersMap(year);
+  return speakersMap.get(speakerId);
Relevance

●● Moderate

Repo has accepted Map-based cached lookups before; one-off Map overhead concern is subjective
without rejection precedent.

PR-#11

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new implementation always builds a Map from the entire speakers array; at least one call site
invokes getSpeakerByYearAndId exactly once to render an OG image, so it won’t benefit from repeated
Map lookups within that invocation.

hooks/useSpeakers.ts[38-46]
app/[year]/speakers/[speakerId]/opengraph-image.tsx[16-20]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Building a `Map` requires iterating the entire speakers array and allocating the map (and currently also an intermediate array via `.map`). For single lookups, `.find()` can short-circuit and allocates less.

### Issue Context
Some call sites appear to do a single lookup per request (e.g. OG image generation), while other paths may do many lookups where a cached map helps.

### Fix Focus Areas
- hooks/useSpeakers.ts[38-46]
- app/[year]/speakers/[speakerId]/opengraph-image.tsx[16-20]

### Suggested fix
Consider keeping `getSpeakerByYearAndId` as a simple `.find()` on the cached speakers array, and introduce/use a separate exported helper for bulk lookups (e.g. `getSpeakersMap(year)` or `getSpeakerByYearAndIdBulk(year, ids)`), so only multi-lookup paths pay the map construction cost.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread hooks/useSpeakers.ts
Comment on lines +38 to +41
const getSpeakersMap = cache(async (year: string | number): Promise<Map<string, Speaker>> => {
const speakers = await getSpeakers(year);
return speakers.find((speaker) => speaker.id === speakerId);
return new Map(speakers.map((speaker) => [speaker.id, speaker]));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. React cache mock collisions 🐞 Bug ☼ Reliability

With this PR, hooks/useSpeakers.ts now has two React.cache-wrapped functions that can be called with
the same single argument (year), but the Jest mock for react.cache uses one shared cache keyed only
by JSON.stringify(args). This can cause cached values for getSpeakers(year) and getSpeakersMap(year)
to overwrite each other (wrong return types / order-dependent tests) under the mock implementation.
Agent Prompt
### Issue description
The Jest mock for `react.cache` in performance tests uses a single shared `cacheMap` keyed only by `JSON.stringify(args)`. After this PR adds `getSpeakersMap(year)` (also `cache`-wrapped) which calls `getSpeakers(year)`, the two cached functions can collide under the same key (e.g. `['2024']`), causing test-only incorrect behavior and potential flakiness.

### Issue Context
Production React `cache` scopes memoization per cached function; the current test mock does not, so adding a second cached function in the same module creates new collision opportunities.

### Fix Focus Areas
- __tests__/hooks_performance.test.ts[17-35]
- __tests__/schedule_performance.test.ts[13-31]

### Suggested fix
Update the mock to maintain a separate `Map` per wrapped function (e.g., create `const cacheMap = new Map()` inside the `cache: (fn) => { ... }` closure, or use a `WeakMap<Function, Map>` keyed by `fn`). If you need to clear caches between tests, track per-function maps and clear them in `beforeEach`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread hooks/useSpeakers.ts
Comment on lines 39 to +41
const speakers = await getSpeakers(year);
return speakers.find((speaker) => speaker.id === speakerId);
return new Map(speakers.map((speaker) => [speaker.id, speaker]));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Duplicate ids change winner 🐞 Bug ≡ Correctness

getSpeakerByYearAndId now builds a Map from the speakers array, which overwrites earlier entries for
duplicate speaker.id values; the previous .find() implementation returned the first match. If the
fetched list contains duplicate IDs, the returned Speaker for the same (year, speakerId) will
change.
Agent Prompt
### Issue description
`new Map(speakers.map(([id, speaker])))` keeps the *last* occurrence for a duplicate `speaker.id`, while the prior `.find()` returned the *first* occurrence. This is a subtle semantic change for duplicate IDs.

### Issue Context
Speaker ID uniqueness is not enforced in-repo (types only declare `id: string`). Defensive behavior should ideally remain stable.

### Fix Focus Areas
- hooks/useSpeakers.ts[38-41]

### Suggested fix
Build the map with a loop that preserves the first occurrence:
```ts
const map = new Map<string, Speaker>();
for (const speaker of speakers) {
  if (!map.has(speaker.id)) map.set(speaker.id, speaker);
}
return map;
```
Optionally, detect duplicates and `console.warn` once per year to surface upstream data issues.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread hooks/useSpeakers.ts
Comment on lines +38 to +45
const getSpeakersMap = cache(async (year: string | number): Promise<Map<string, Speaker>> => {
const speakers = await getSpeakers(year);
return speakers.find((speaker) => speaker.id === speakerId);
return new Map(speakers.map((speaker) => [speaker.id, speaker]));
});

export const getSpeakerByYearAndId = async (year: string | number, speakerId: string): Promise<Speaker | undefined> => {
const speakersMap = await getSpeakersMap(year);
return speakersMap.get(speakerId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

3. Map build removes early-exit 🐞 Bug ➹ Performance

getSpeakerByYearAndId now always constructs a full Map of all speakers for the year before doing a
single lookup, eliminating the early-exit behavior of the previous .find() for one-off calls. This
can add unnecessary CPU/memory on call sites that only need one speaker (e.g., OpenGraph image
generation).
Agent Prompt
### Issue description
Building a `Map` requires iterating the entire speakers array and allocating the map (and currently also an intermediate array via `.map`). For single lookups, `.find()` can short-circuit and allocates less.

### Issue Context
Some call sites appear to do a single lookup per request (e.g. OG image generation), while other paths may do many lookups where a cached map helps.

### Fix Focus Areas
- hooks/useSpeakers.ts[38-46]
- app/[year]/speakers/[speakerId]/opengraph-image.tsx[16-20]

### Suggested fix
Consider keeping `getSpeakerByYearAndId` as a simple `.find()` on the cached speakers array, and introduce/use a separate exported helper for bulk lookups (e.g. `getSpeakersMap(year)` or `getSpeakerByYearAndIdBulk(year, ids)`), so only multi-lookup paths pay the map construction cost.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant