Sander Korf
  • Portfolio
  • Resume

Sander Korf

Dutch Full Stack AI Engineer helping businesses cut costs through intelligent automation. 13+ years turning expensive manual processes into systems that work 24/7. Based in Amsterdam, available for freelance projects.

Navigation

  • Portfolio
  • Resume

Legal

  • Cookies
  • Privacy Policy
  • Terms and Conditions

Blog

Algolia

  • Algolia InstantSearch INP and object identity
  • Algolia merch by Sanity _type, not title
  • Algolia InstantSearch search key, not admin

Vercel

  • Next.js empty Suspense fallbacks wreck CLS P95
  • Vercel Workflows: skip closed eToro legs

eToro

  • eToro deploy vs rotate: idle cash not rotate
  • eToro search 404: query fields, not ticker
  • eToro v2 opens at-most-once, settlement
  • eToro LLM overlay ±10pp: critic cannot veto

Next.js

  • next-intl useTranslate skip links above fold
  • next-intl owns Klaviyo nl-NL email copy

Firebase & Expo

  • Staging deep links must not hijack production
  • Turn off Worklets Bundle Mode for EAS SHA-1
  • Reclaim Firebase orphan without password wipe

Centra & Klaviyo

  • Centra to Klaviyo tags need a plugin contract

About

  • Full Stack AI Engineer in Amsterdam

© 2026 Sander Korf. All rights reserved

94719489

  • Applied AI
  1. Home
  2. Blog
  3. Algolia InstantSearch INP and object identity

Algolia InstantSearch INP and object identity

I kept reopening an analytics span on every filter tap because a fresh empty object retriggered the effect. Stabilize refs and key off the query id.

Sander KorfPublished August 31, 20263 min read
algoliainstantsearchnextjsshopifyopentelemetry

This was a Shopify storefront, not a sandbox PLP

Fashion catalog on Next.js. Algolia InstantSearch ran the hits. Shopify owned the cart. OpenTelemetry logged the search span. If a filter chip felt sticky, the shopper did not think "object identity." They thought the site was cheap.

What they would notice if it failed

A tap on size or color that hangs. INP that looks like a frozen grid. The virtualizer was already heavy. One extra effect per render is how a clothing PLP starts to feel like a spreadsheet.

Algolia InstantSearch paid INP for a new object

I was on Shopify collection product listing pages in a Next.js catalog. Algolia InstantSearch drove the hits. A virtualizer kept the long list from mounting every card. Quick filters sat above the grid. On paper that stack is fine. In the field, every chip tap felt sticky. INP was paying for work that should have been a no-op.

I am not rewriting the virtualizer in this post. The list was already heavy. Two extra cuts sat on top of it, and they were cheaper to remove than a virtualizer rewrite.

The logger rode every client section

The first cut was the section error boundary. It statically imported the OpenTelemetry logger on every client section. A render failure is rare. The logger is not cheap. Every section paid for it on the default graph, including the Algolia InstantSearch block that already struggled with INP.

I moved reportSectionRenderFailed behind import(). Next.js then kept the happy path thin. The logger loads when a section actually fails. That is the right time to pay for it.

A default empty object retriggered the span

The second cut was useAlgoliaObservability. It opened an algolia.search span when Algolia InstantSearch returned results. The hook took additionalAttributes and results as effect dependencies. It also defaulted additionalAttributes to {}. A default empty object is a new object every call. InstantSearch handed us a new results object on every render, even when queryID had not changed.

The effect retriggered. The span opened again. Filters and the virtualizer already had enough to do. We added a span open on top of every render. That is how object identity shows up as INP.

Put the objects in refs

The fix is boring. Keep additionalAttributes and results in refs. Update the refs during render. Depend only on results?.queryID, indexName, locale, and componentName. When queryID is missing or unchanged, return. Read the latest objects from the refs inside the effect. At the call site, memoize { 'search.has_filters': canClearRefinements } so we do not allocate a new attrs object for no reason.

export function useAlgoliaObservability({ indexName, results, locale, componentName, additionalAttributes }: Options) {
	const lastQueryIDRef = useRef<string | undefined>(undefined);
	const additionalAttributesRef = useRef(additionalAttributes);
	additionalAttributesRef.current = additionalAttributes;
	const resultsRef = useRef(results);
	resultsRef.current = results;
 
	useEffect(() => {
		const current = resultsRef.current;
		if (!current?.queryID || current.queryID === lastQueryIDRef.current) return;
		lastQueryIDRef.current = current.queryID;
		// open the algolia.search span from current + additionalAttributesRef.current
	}, [results?.queryID, indexName, locale, componentName]);
}

Object identity is not value identity. INP notices the difference. Click "same query, new objects" in the demo. The unstable side fires the effect. The stable side does not. Click "new queryID" and both fire. That is the lesson I wanted in the hook.

Effect fires vs object identity

Local demo. Same query with new objects retriggers unstable deps. Stable deps wait for queryID.
queryID: q-1hits: 24search.has_filters: false

Unstable deps

Effect depends on results and additionalAttributes.

0

effect fires

Stable queryID deps

Latest values via useEffectEvent. Effect depends on queryID.

0

effect fires

Same query, new objects should only bump the left counter. New queryID bumps both.


Happy coding!
Sander