Insights into Company
← React

Performance and Testing

Understand the idea. Compare it. Explain it. Test yourself.

Question 1 of 5

Why does a component re-render?

Solution and explanation

The triggers

A component re-renders when:

  1. **Its state changes** (setState with a different value).
  2. **Its parent re-renders** (by default every child re-renders too, even with the same props).
  3. **A context it consumes changes value.**
  4. **A custom hook it uses** changes its internal state.

Props changing is not a separate trigger: props only change because the parent re-rendered.

Re-rendering means "React calls the function again and diffs the result". It does not necessarily change the DOM. Re-renders are usually cheap; they become a problem only when a large tree or expensive calculation re-runs often.

Example

function Page() {
  const [query, setQuery] = useState('');
  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ExpensiveChart data={chartData} />   {/* re-renders on every keystroke */}
    </>
  );
}

Fixes

  • **Move state down:** put query and the input in their own <SearchBox> so Page does not re-render.
  • **Pass content as children:** components given as children from above are not re-created by the state owner.
  • **React.memo** on ExpensiveChart, with stable props (useMemo/useCallback).
  • **Split contexts** or use selector-based stores so consumers only re-render for the slice they use.
  • **useDeferredValue / startTransition** to keep typing responsive while a heavy list updates.

Finding unnecessary re-renders

  • React DevTools Profiler: "Highlight updates when components render" and "Why did this render?".
  • console.count('JobRow render') during development.

Summary

State changes, parent renders and context changes cause re-renders; keep state close to where it is used, and memoise only where profiling shows real cost.

Practice explaining this
Question 2 of 5

How does React.memo work?

Solution and explanation

What it does

React.memo(Component) returns a memoised component that **skips re-rendering when its props are shallowly equal** to the previous props.

const JobRow = React.memo(function JobRow({ job, onApply }) {
  console.log('render', job.id);
  return <li>{job.title} <button onClick={() => onApply(job.id)}>Apply</button></li>;
});

Shallow comparison

Each prop is compared with Object.is:

  • Primitives ('abc', 5, true) compare by value.
  • Objects, arrays and functions compare by **reference**. A new object or arrow function each render breaks memo.
function JobList({ jobs }) {
  const [selected, setSelected] = useState(null);

  // Breaks memo: new function every render
  // <JobRow onApply={id => apply(id)} />

  const handleApply = useCallback(id => apply(id), []);
  return jobs.map(j => <JobRow key={j.id} job={j} onApply={handleApply} />);
}

Now selecting an item re-renders JobList, but unchanged JobRows skip rendering.

Custom comparison

export default React.memo(JobRow, (prev, next) => prev.job.updatedAt === next.job.updatedAt);

Return true to skip the render. Use carefully: forgetting a prop causes stale UI.

What memo does not stop

  • Re-renders from the component's **own** state or a context it uses.
  • Changes in children (JSX children are new objects every time).

When to use it

  • Rows of long lists, charts, editors: expensive to render and often given the same props.
  • Not for small, cheap components: comparing props then costs more than rendering.

Summary

React.memo skips a re-render when props are shallowly equal; it works only if objects and callbacks passed in are kept stable with useMemo and useCallback.

Practice explaining this
Question 3 of 5

What is code splitting?

Solution and explanation

What it is

Code splitting breaks your JavaScript bundle into smaller chunks that load **on demand**, so the first page downloads only the code it needs. A single 3 MB bundle slows the first load on mobile networks; splitting by route often cuts it to a few hundred KB.

Route-based splitting with React.lazy

import { lazy, Suspense } from 'react';
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const ResumeBuilder  = lazy(() => import('./pages/ResumeBuilder'));

<Suspense fallback={<Spinner />}>
  <Routes>
    <Route path="/admin" element={<AdminDashboard />} />
    <Route path="/resume" element={<ResumeBuilder />} />
  </Routes>
</Suspense>

The bundler (Vite, webpack) creates a separate file for each import(). It is downloaded the first time the route renders; Suspense shows the fallback meanwhile.

Component-level splitting

Load heavy widgets only when needed:

const PdfPreview = lazy(() => import('./PdfPreview'));   // pdf.js is large
{showPreview && <Suspense fallback="Loading preview…"><PdfPreview file={file} /></Suspense>}

Library-level splitting

button.onclick = async () => {
  const { utils, writeFile } = await import('xlsx');   // only when exporting
  writeFile(utils.book_new(), 'jobs.xlsx');
};

Good practices

  • Split by route first; that gives the biggest win.
  • **Prefetch** likely next routes on hover (import('./pages/Job') in onMouseEnter).
  • Analyse bundles with vite-bundle-visualizer or webpack-bundle-analyzer; replace heavy libraries (moment.js → date-fns or dayjs).
  • Wrap lazy routes in an **error boundary** to handle chunk load failures after a deployment.

Summary

Code splitting with dynamic import(), React.lazy and Suspense loads code only when needed, reducing initial load time.

Practice explaining this
Question 4 of 5

How do you test user behavior?

Solution and explanation

Principle

Test what the user sees and does, not implementation details. **React Testing Library (RTL)** encourages this: find elements by role, label or text, interact like a user, and assert on visible results. It runs with **Vitest** or **Jest**.

Example

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

test('shows an error for a wrong password', async () => {
  const user = userEvent.setup();
  const onLogin = vi.fn().mockRejectedValue(new Error('Invalid credentials'));
  render(<LoginForm onLogin={onLogin} />);

  await user.type(screen.getByLabelText(/email/i), 'asha@example.com');
  await user.type(screen.getByLabelText(/password/i), 'wrong');
  await user.click(screen.getByRole('button', { name: /sign in/i }));

  expect(onLogin).toHaveBeenCalledWith('asha@example.com', 'wrong');
  expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();
});

Queries, in order of preference

  1. getByRole('button', { name: 'Apply' }): also checks accessibility.
  2. getByLabelText, getByPlaceholderText, getByText.
  3. getByTestId only as a last resort.

getBy… throws if missing, queryBy… returns null (assert absence), findBy… waits for async elements.

Mocking the network

Use **MSW (Mock Service Worker)** to intercept fetch at the network level, so components run their real data-fetching code:

http.get('/api/jobs', () => HttpResponse.json([{ id: 1, title: 'React Developer' }]))

Test pyramid for a React app

  • **Unit:** pure functions, reducers, hooks (renderHook).
  • **Component/integration:** RTL tests of forms, lists, flows with MSW.
  • **End-to-end:** **Playwright** or Cypress in a real browser for critical journeys (sign up, apply to a job, payment).

What not to test

Internal state values, private functions, CSS class names, or that useState was called.

Summary

Render the component, act like a user with userEvent, find elements by role or label, mock the network with MSW, and assert on what the user sees.

Practice explaining this
Question 5 of 5

How do you diagnose a slow React page?

Solution and explanation

Step 1: measure, do not guess

  • **Lighthouse / PageSpeed Insights:** LCP (largest contentful paint), INP (interaction to next paint), CLS (layout shift), bundle size.
  • **Chrome DevTools Performance tab:** record the slow interaction; look for long tasks (> 50 ms) and what JavaScript runs.
  • **React DevTools Profiler:** which components rendered, how long, and why.
  • **Network tab:** slow or waterfall API calls, large images, too many requests.

Step 2: identify the type of slowness

SymptomLikely causeFix
Slow first loadbig bundle, render-blocking scriptscode splitting, tree shaking, lighter libraries, SSR
Spinner for longslow or sequential API callsparallel requests, caching (TanStack Query), backend indexing, pagination
Typing lagsbig tree re-renders per keystrokemove state down, React.memo, useDeferredValue, debounce
Scrolling a long list is jankythousands of DOM nodesvirtualisation (react-window, TanStack Virtual)
Layout jumpsimages without dimensions, late fontsset width/height, font-display: swap, skeletons
Gets slower over timememory leakclean up effects, listeners, intervals

Step 3: common fixes in code

// Virtualise a 10,000-row list
import { FixedSizeList } from 'react-window';
<FixedSizeList height={600} itemCount={jobs.length} itemSize={72} width="100%">
  {({ index, style }) => <div style={style}><JobRow job={jobs[index]} /></div>}
</FixedSizeList>

// Keep input responsive while filtering
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => search(jobs, deferredQuery), [jobs, deferredQuery]);
  • Images: modern formats (WebP/AVIF), loading="lazy", correct sizes, a CDN.
  • Avoid request waterfalls: start fetches in parallel (Promise.all) or at the route level.

Step 4: verify

Re-run the profiler and Lighthouse after each change and compare numbers. Add a performance budget in CI (bundle size limit) so it does not regress.

Summary

Measure with Lighthouse, the Performance tab and the React Profiler; classify the problem (load, network, render, list size, memory); apply the targeted fix and measure again.

Practice explaining this