This content originally appeared on DEV Community and was authored by Aldorax
Today, I came across a post on dev.to from Fatemeh Paghar on using useTransition and react-window. I decided to push a typical React search/filter UI to the extreme by scaling it up to handle 100,000 users — not a typo — one hundred thousand records client-side.
Instead of simple emails, each user now has:
- A name
- A skillset (e.g., React, Python, AWS)
To make it even more interesting, I added a second search field — but with a twist:
You must first search by name before the skill search field becomes active.
🛠 How It Works
- First input: Search by name (e.g., “Alice”, “Bob”)
- Second input: Search by skill (e.g., “React”, “Rust”), but it remains disabled until a name is entered.
Once a name is typed, the skill search input unlocks and users can further refine their search.
⚡ First: The Basic Naive Version (No Optimization)
Let's look at a simple implementation first — without react-window
and without useMemo
.
"use client"
import React, { useState, useTransition } from "react";
const generateUsers = (count: number) => {
const names = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Helen"];
const skills = ["React", "Python", "Node.js", "Django", "Rust", "Go", "Flutter", "AWS"];
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `${names[i % names.length]} ${i}`,
skill: skills[i % skills.length],
}));
};
const usersData = generateUsers(100000);
export default function BasicMassiveSearch() {
const [nameQuery, setNameQuery] = useState("");
const [skillQuery, setSkillQuery] = useState("");
const [isPending, startTransition] = useTransition();
const handleNameSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
startTransition(() => setNameQuery(value));
};
const handleSkillSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
startTransition(() => setSkillQuery(value));
};
const filteredUsers = usersData.filter((user) => {
const matchesName = user.name.toLowerCase().includes(nameQuery.toLowerCase());
if (!matchesName) return false;
if (skillQuery) {
return user.skill.toLowerCase().includes(skillQuery.toLowerCase());
}
return true;
});
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">Simple Skill Search</h1>
<input
type="text"
value={nameQuery}
onChange={handleNameSearch}
placeholder="Search by name..."
className="border p-2 mb-4 block w-full"
/>
<input
type="text"
value={skillQuery}
onChange={handleSkillSearch}
disabled={!nameQuery}
placeholder="Search by skill..."
className="border p-2 mb-4 block w-full"
/>
{isPending && <p className="italic text-sm">Filtering...</p>}
<div className="mt-4">
{filteredUsers.length === 0 ? (
<p className="italic text-gray-500">No users found.</p>
) : (
filteredUsers.map((user) => (
<div key={user.id} className="p-2 border-b">
<p className="font-bold">{user.name}</p>
<p className="text-sm text-gray-600">{user.skill}</p>
</div>
))
)}
</div>
</div>
);
}
😵 Problems With This Naive Version
- No Virtualization: Every single result (up to 100,000 DOM elements) renders at once. ➔ Browser lags, page freezes, and scrolling becomes painful.
-
No Memoization:
Every re-render recomputes
.filter()
unnecessarily. ➔ Causes typing delays as the dataset grows. - Performance collapse: Mobile users and low-end laptops will suffer first.
✅ Now: The Optimized Version (Virtualized + Memoized)
"use client"
import React, { useState, useMemo, useTransition } from "react";
import { FixedSizeList as List } from "react-window";
const generateUsers = (count: number) => {
const names = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Helen"];
const skills = ["React", "Python", "Node.js", "Django", "Rust", "Go", "Flutter", "AWS"];
return Array.from({ length: count }, (_, i) => ({
id: i,
name: `${names[i % names.length]} ${i}`,
skill: skills[i % skills.length],
}));
};
const usersData = generateUsers(100000);
export default function MassiveSkillSearch() {
const [nameQuery, setNameQuery] = useState("");
const [skillQuery, setSkillQuery] = useState("");
const [isPending, startTransition] = useTransition();
const [searchDuration, setSearchDuration] = useState<number>(0);
const filteredUsers = useMemo(() => {
const start = performance.now();
const lowerName = nameQuery.toLowerCase();
const lowerSkill = skillQuery.toLowerCase();
const result = usersData.filter((user) => {
const matchesName = user.name.toLowerCase().includes(lowerName);
if (!matchesName) return false;
if (lowerSkill) {
return user.skill.toLowerCase().includes(lowerSkill);
}
return true;
});
const end = performance.now();
setSearchDuration(end - start);
return result;
}, [nameQuery, skillQuery]);
const handleNameSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
startTransition(() => setNameQuery(value));
};
const handleSkillSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
startTransition(() => setSkillQuery(value));
};
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => {
const user = filteredUsers[index];
return (
<div style={style} key={user.id} className="p-3 border-b border-green-100 hover:bg-green-50">
<p className="font-medium text-gray-800">{user.name}</p>
<p className="text-sm text-gray-500">{user.skill}</p>
</div>
);
};
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">🧠 Skillset Search (Virtualized)</h1>
<input
type="text"
value={nameQuery}
onChange={handleNameSearch}
placeholder="Search by name..."
className="border p-2 mb-4 block w-full"
/>
<input
type="text"
value={skillQuery}
onChange={handleSkillSearch}
disabled={!nameQuery}
placeholder="Search by skill..."
className="border p-2 mb-4 block w-full"
/>
{isPending && <p className="italic text-sm">Filtering...</p>}
{filteredUsers.length === 0 ? (
<p className="italic text-gray-500">No users found.</p>
) : (
<List height={500} itemCount={filteredUsers.length} itemSize={70} width="100%">
{Row}
</List>
)}
<div className="mt-6 text-sm text-gray-600 text-center">
<p>Total users: {usersData.length}</p>
<p>Filtered users: {filteredUsers.length}</p>
<p>Last search took: {searchDuration.toFixed(2)} ms</p>
<p>{isPending ? "Searching..." : "Idle"}</p>
</div>
</div>
);
}
🧠 What Changed — Why It Matters
Feature | Basic Version | Optimized Version |
---|---|---|
Rendering | 100,000 real DOM nodes | Only ~20 visible DOM nodes |
Filtering | Recalculates .filter() every render |
Memorized with useMemo , recalculates only when query changes |
Scrolling | Laggy and crash-prone | Smooth and buttery |
Typing | Freezes momentarily on big datasets | Remains fluid |
CPU Usage | Very high | Low and efficient |
🏁 Final Verdict
✅ If you are just prototyping or handling small datasets (few hundred items),
you can survive without virtualization and memoization.
❗ But the moment you deal with tens of thousands of records (or more),
you must:
- Virtualize large lists
- Use
useMemo
smartly - Possibly move search server-side
Otherwise, your app will become unusable — fast.
🎯 Closing Note
React gives you amazing flexibility, but scale tests your architecture.
Always think about memory, CPU, and user experience as you grow datasets!
This content originally appeared on DEV Community and was authored by Aldorax

Aldorax | Sciencx (2025-04-26T01:30:18+00:00) 🚀 Scaling Client-Side Search: 100,000 Users, Skills, and Real-Time Filtering in React. Retrieved from https://www.scien.cx/2025/04/26/%f0%9f%9a%80-scaling-client-side-search-100000-users-skills-and-real-time-filtering-in-react/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.