Loading...
Loading...
MUST be used whenever optimizing a Dune app for speed, reducing render counts, improving CDF query efficiency, or reducing bundle size. Do NOT skip measurement steps — always profile before changing code. Triggers: performance, slow, laggy, optimize, optimization, re-render, bundle size, load time, Lighthouse, profiler, virtualization, lazy load, code split, CDF query, large list, memory leak.
npx skill4agent add cognitedata/dune-skills performancepnpm run build
pnpm run previewsrc/App.tsx// BAD — new object on every render causes children to re-render
<Chart options={{ color: "red" }} />
// GOOD
const chartOptions = useMemo(() => ({ color: "red" }), []);
<Chart options={chartOptions} />// BAD
<Button onClick={() => doSomething(id)} />
// GOOD
const handleClick = useCallback(() => doSomething(id), [id]);
<Button onClick={handleClick} />// BAD — new object reference every render
<MyContext.Provider value={{ user, sdk }}>
// GOOD — memoize the context value
const ctxValue = useMemo(() => ({ user, sdk }), [user, sdk]);
<MyContext.Provider value={ctxValue}>grep -rn --include="*.tsx" \
-E "value=\{\{|onClick=\{\(\)" src/React.memosdk.client.useQueryuseCogniteClient| Issue | Fix |
|---|---|
No | Add |
| Fetching all properties | Add a |
| Fetching on every render | Move inside |
| Sequential requests that could be parallel | Use |
| Polling without debounce | Add debounce (300–500 ms) for search inputs before firing CDF queries |
// BAD — fetches everything
const result = await client.instances.list({ instanceType: "node" });
// GOOD — scoped and limited
const result = await client.instances.list({
instanceType: "node",
sources: [{ source: { type: "view", space: "my-space", externalId: "Asset", version: "v1" } }],
filter: { equals: { property: ["node", "space"], value: "my-space" } },
limit: 100,
});grep -rn --include="*.tsx" -E "\.(map|forEach)\(" src/.map()@tanstack/react-virtualimport { useVirtualizer } from "@tanstack/react-virtual";
const rowVirtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: rowVirtualizer.getTotalSize() }}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<div
key={virtualRow.key}
style={{ transform: `translateY(${virtualRow.start}px)` }}
>
{items[virtualRow.index].name}
</div>
))}
</div>
</div>
);// BEFORE
import { ReportPage } from "./pages/ReportPage";
// AFTER
import { lazy, Suspense } from "react";
const ReportPage = lazy(() => import("./pages/ReportPage"));
// In the route:
<Suspense fallback={<PageSkeleton />}>
<ReportPage />
</Suspense># Install if not already present, then run
pnpm add -D rollup-plugin-visualizervite.config.tsimport { visualizer } from "rollup-plugin-visualizer";
export default defineConfig({
plugins: [
react(),
visualizer({ open: true, gzipSize: true, brotliSize: true }),
],
});pnpm run buildlodashlodash-esmomentdate-fnsIntluseEffectgrep -rn --include="*.tsx" --include="*.ts" -A 10 "useEffect" src/useEffectaddEventListenersetIntervalsetTimeoutsubscribeuseEffect(() => {
const controller = new AbortController();
fetchData(controller.signal);
return () => controller.abort();
}, [id]);| Metric | Before | After | Change |
|---|---|---|---|
| Lighthouse Performance | 72 | 91 | +19 |
| Largest Contentful Paint | 3.2 s | 1.8 s | −1.4 s |
| Total Blocking Time | 420 ms | 80 ms | −340 ms |
| Bundle size (gzipped) | 410 KB | 290 KB | −120 KB |
| 8 | 2 | −6 |