DEV Community

Cover image for Practice Frontend Performance Interviews With a Node.js Drill
Karuha
Karuha

Posted on • Originally published at aceround.app

Practice Frontend Performance Interviews With a Node.js Drill

Most frontend performance interview answers fail for the same reason: they jump straight to a fix. A stronger answer sounds like a debugging session: name the user-visible symptom, choose a metric, inspect the likely bottleneck, explain the trade-off, then propose the smallest safe change.

Here is a small Node.js drill you can run before a frontend interview to practice that shape.

Frontend developer interview practice visual

Why performance questions are different

Algorithm questions usually have an expected answer. Frontend performance questions rarely do.

If an interviewer says, "This product page feels slow after a React refactor," they are not waiting for the word useMemo. They are checking whether you can reason from symptoms to evidence:

  • Is the problem network, JavaScript execution, rendering, or layout?
  • Which user metric changed: LCP, CLS, INP, TTFB, or something local to the app?
  • What changed recently: bundle size, API shape, image dimensions, hydration, third-party scripts?
  • Can you fix the issue without making accessibility, maintainability, or product behavior worse?

That is why rereading Web Vitals docs helps, but only up to a point. You need reps speaking the diagnostic path out loud.

The answer shape I practice

For a live interview, I like this five-step answer:

Step What to say
Symptom "Users see X, and the metric that likely maps to it is Y."
First measurement "I would inspect A before changing code because it separates B from C."
Likely cause "Given the constraints, the most likely bottleneck is..."
Fix path "I would try the smallest reversible change first..."
Follow-up "Then I would verify with metric Z and watch for regression W."

That sounds basic, but it prevents the two common failure modes: guessing a favorite optimization, or listing every tool you know without making a decision.

A runnable drill

Save this as frontend-performance-drill.mjs and run it with Node 18 or newer:

#!/usr/bin/env node
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

const scenarios = [
  {
    title: "Product page feels slow after a React refactor",
    prompt:
      "Largest Contentful Paint moved from 2.1s to 4.8s on mid-range Android. The bundle grew by 180KB and image bytes are unchanged. Walk through your first investigation.",
    signals: ["lcp", "bundle", "code splitting", "network", "main thread", "profiling"],
    followUp:
      "The React profiler shows one expensive component rendering three times before the hero appears. What do you check next?",
  },
  {
    title: "Infinite list janks during scroll",
    prompt:
      "A feed scrolls smoothly on desktop but drops frames on mobile after 80 items. API latency is fine. Explain the likely bottleneck and a fix path.",
    signals: ["virtualization", "dom nodes", "layout", "paint", "memo", "windowing"],
    followUp:
      "The interviewer says virtualization is not allowed for accessibility reasons. What is your fallback?",
  },
  {
    title: "Search box sends too many requests",
    prompt:
      "Typing in a search input fires a request on every keypress. Users see stale results overwrite fresh results. Design the client-side control flow.",
    signals: ["debounce", "abortcontroller", "race", "cache", "loading state", "idempotent"],
    followUp:
      "Now make the same answer safe for a flaky mobile network.",
  },
  {
    title: "CLS spike after marketing adds a banner",
    prompt:
      "Cumulative Layout Shift jumps from 0.04 to 0.21 after a campaign banner ships. You cannot remove the banner. What do you change?",
    signals: ["cls", "reserved space", "dimensions", "fonts", "animation", "measurement"],
    followUp:
      "The banner height is personalized. How do you reserve space without hard-coding one number?",
  },
];

function pickRounds(count) {
  return [...scenarios].sort(() => Math.random() - 0.5).slice(0, count);
}

function score(answer, signals) {
  const text = answer.toLowerCase();
  const hits = signals.filter((signal) => text.includes(signal));
  return {
    hits,
    missed: signals.filter((signal) => !hits.includes(signal)),
    score: Math.round((hits.length / signals.length) * 100),
  };
}

function printRubric(result, scenario) {
  console.log("\nRubric");
  console.log(`- Coverage: ${result.score}%`);
  console.log(`- Signals you mentioned: ${result.hits.join(", ") || "none"}`);
  console.log(`- Signals to add: ${result.missed.join(", ") || "none"}`);
  console.log(`- Follow-up to practice: ${scenario.followUp}`);
}

async function main() {
  const roundsArg = process.argv.find((arg) => arg.startsWith("--rounds="));
  const rounds = Number(roundsArg?.split("=")[1] ?? 2);
  const rl = readline.createInterface({ input, output });

  console.log("Frontend performance interview drill");
  console.log("Answer like you would in a live interview: diagnose, trade off, then propose a safe fix.\n");

  for (const scenario of pickRounds(rounds)) {
    console.log(`Scenario: ${scenario.title}`);
    console.log(scenario.prompt);
    const answer = await rl.question("\nYour answer: ");
    printRubric(score(answer, scenario.signals), scenario);
    console.log("\n---\n");
  }

  rl.close();
}

main().catch((error) => {
  console.error("Drill failed", error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Run it:

node frontend-performance-drill.mjs --rounds=2
Enter fullscreen mode Exit fullscreen mode

The score is intentionally simple. It is not trying to judge whether your answer is "correct." It checks whether you remembered to mention the signals that usually separate a shallow answer from a senior one.

How to answer one scenario well

Take the LCP scenario:

Largest Contentful Paint moved from 2.1s to 4.8s on mid-range Android. The bundle grew by 180KB and image bytes are unchanged.

A weak answer:

I would use lazy loading and memoization.

A stronger answer:

Since LCP regressed and image bytes did not change, I would first separate network delay from main-thread delay. I would compare the waterfall and the React profiler around the hero render. The 180KB bundle increase suggests parsing or hydration might be delaying the LCP element, so I would check whether the refactor pulled a heavy dependency into the critical path. If so, I would try route-level code splitting or moving non-critical logic behind interaction. Then I would verify LCP on a throttled mid-range profile, not just my laptop.

Notice the difference. The stronger answer has a metric, a hypothesis, a measurement path, a fix, and a verification step. You do not need perfect wording. You need a defensible sequence.

Add your own scenarios

The drill becomes more useful when you add scenarios from your own projects. Good prompts usually include one metric, one constraint, and one misleading detail.

Examples:

  • "INP is bad only on checkout. The slow interaction is a coupon field, but the API is fast."
  • "A modal animation causes jank on low-end devices. Design wants to keep the animation."
  • "A dashboard moved from server-rendered HTML to client rendering. TTFB improved, but users say the page feels slower."
  • "A third-party analytics script is required by the business and blocks the main thread."

For each one, add six signals you want your answer to include. Use lowercase words because the scorer does simple substring matching.

{
  title: "Coupon field has poor INP",
  prompt:
    "Interaction to Next Paint is poor when users type in the coupon field. The coupon API responds in 80ms. What do you inspect?",
  signals: ["inp", "debounce", "main thread", "render", "profiling", "state"],
  followUp:
    "The input lives inside a large form provider. How do you reduce re-renders without rewriting the form?",
}
Enter fullscreen mode Exit fullscreen mode

When AI practice helps

For frontend interviews, AI practice is most useful when it plays the annoying interviewer: it asks the follow-up you hoped nobody would ask.

That is also how I use aceround.app - AI interview assistant for frontend prep: not to memorize a perfect answer, but to rehearse the moment where the interviewer says, "Okay, but what would you measure first?"

Keep the tool honest. If it gives you a polished answer you would never say, rewrite it in your own voice. Interviewers can tell when a candidate is reciting.

What to measure in real projects

If you want the drill to map back to production work, anchor your scenarios to real browser signals:

  • LCP: usually hero image, heading, or large visible block delayed by network or main-thread work.
  • CLS: usually missing dimensions, injected content, font swaps, or animations that change layout.
  • INP: usually event handlers, synchronous state updates, expensive re-renders, or long tasks.
  • Bundle growth: often hidden in a refactor that imports a full library for one helper.
  • Scroll jank: often too many DOM nodes, forced layout, expensive paint, or repeated work in scroll handlers.

The exact thresholds change as browsers and guidance evolve, so check the current Web Vitals docs before quoting a number in an interview. But the reasoning pattern stays stable.

FAQ

Should I memorize Web Vitals thresholds?

Know the rough meaning of LCP, CLS, and INP, but do not turn the interview into trivia. A candidate who can explain how they would investigate a bad LCP is usually more convincing than a candidate who only remembers a threshold.

Is useMemo a good performance interview answer?

Only after you have evidence that repeated computation or referential instability is the bottleneck. If your first answer to every performance question is useMemo, it sounds like guessing.

Should I mention Lighthouse?

Yes, but as one input. Lighthouse is useful for lab diagnostics, while production telemetry shows what real users experience. A strong answer explains when you would use each.

How long should I practice this?

Do 10 scenarios, two minutes each. Record yourself once. The recording is uncomfortable, but it shows whether you are explaining trade-offs or just naming tools.

Sources and disclosure

Useful references: Google Web Vitals documentation, MDN Performance APIs, React Profiler documentation, and the frontend interview preparation notes linked above.

Disclosure: I used AI assistance to outline and edit parts of this post. The code was run locally with Node 22 before publishing.

Top comments (0)