High-Frequency AI Frontend / Agent Engineer Interview Questions and Sample Answer Scripts
AI Practice · Interview Question Bank · Agent Engineering
AI Frontend / Agent Engineer Interview Bank
This is a public, cleaned-up interview-prep guide for AI frontend and agent engineering roles. It keeps the useful parts of the original question bank: what gets asked often, how to answer clearly, where candidates usually get pushed deeper, and what mistakes to avoid.
The numbers below are approximate mention rates across near one hundred real interview write-ups. Treat them as a preparation priority map, not as hard statistics. The higher the mention rate, the more likely the topic deserves a ready-to-say answer.
How to use this guide
- First tier, above 50%: streaming rendering, token counting, RAG, SSE, Promise, performance, state management, and concurrency control. These should be automatic.
- Second tier, 30–50%: Fiber, event loop, XSS and prompt injection, vector search, Web Worker, memory leaks, and build tooling. These separate strong candidates from average ones.
- Third tier, below 30%: detailed Webpack/Vite internals, manual coding questions, and HR questions. Prepare enough to avoid blanking out.
Global high-frequency list
| # | Mention rate | Area | Topic |
|---|---|---|---|
| 1 | 95% | Engineering | Frontend observability and event tracking |
| 2 | 84% | AI frontend | Streaming rendering and real-time Markdown parsing |
| 3 | 82% | JavaScript | Promise, async, and await |
| 4 | 78% | Network | Cookie, session, token, and JWT |
| 5 | 78% | Performance | Frontend performance optimization |
| 6 | 78% | Performance | Virtual scrolling for long lists |
| 7 | 77% | CSS | Selectors and specificity |
| 8 | 77% | AI frontend | Token counting and context budgeting |
| 9 | 77% | Architecture | System design and technical selection |
| 10 | 75% | CSS | Positioning, stacking context, and containing blocks |
| 11 | 75% | AI frontend | RAG and source-grounded generation |
| 12 | 71% | AI frontend | SSE versus WebSocket in AI products |
| 13 | 70% | React | Redux, Zustand, and state management |
| 14 | 70% | Performance | FP, FCP, LCP, INP, and CLS |
| 15 | 70% | Coding | Promise concurrency control |
| 16 | 68% | AI frontend | Citations, source tracing, and confidence |
| 17 | 67% | Coding | Debounce and throttle |
| 18 | 62% | Performance | Web Worker |
| 19 | 59% | Security | AI content XSS and prompt injection |
| 20 | 55% | React | Fiber architecture |
| 21 | 53% | JavaScript | Garbage collection and memory leaks |
| 22 | 53% | AI frontend | ReadableStream and TextDecoder |
| 23 | 52% | React | memo, useMemo, and render optimization |
| 24 | 51% | React | Virtual DOM and diffing |
| 25 | 48% | JavaScript | Event loop |
| 26 | 45% | AI frontend | Typewriter effect |
| 27 | 42% | AI frontend | Hallucination handling |
| 28 | 40% | Engineering | Canary release and rollback |
| 29 | 38% | AI frontend | Multi-turn conversation state |
| 30 | 35% | HR | Core strengths and career gap explanation |
Every high-frequency question has three parts: a sample answer, an Interviewer follow-ups block listing the drill-downs that actually come up, and a My take block with the judgement calls and war stories I lean on. For the top-mention topics, prepare all three.
JavaScript Fundamentals
1. Promise / async await #JavaScript 82%
Typical question: When calling several AI model APIs, how do you choose between Promise.all, Promise.allSettled, and sequential await?
One-line answer: Promise is the state container for async work. async/await is syntax over Promise that makes control flow read like synchronous code, while errors can be handled with try/catch.
- Use sequential
awaitwhen each step depends on the previous result, such as planning → retrieval → answer generation. - Use
Promise.allwhen all tasks must succeed together. One rejection fails the whole group. - Use
Promise.allSettledwhen partial success is useful, such as querying multiple models or multiple retrieval sources. - Use
AbortControllerfor cancellation. Promise itself is not cancellable.
type ModelName = 'fast-model' | 'reasoning-model';
type ModelAnswer = { text: string; model: ModelName };
type AbortableTask<T> = (signal: AbortSignal) => Promise<T>;
async function withTimeout<T>(task: AbortableTask<T>, ms: number): Promise<T> {
const controller = new AbortController();
const timerId = window.setTimeout(() => controller.abort(), ms);
try {
return await task(controller.signal);
} finally {
window.clearTimeout(timerId);
}
}
const results = await Promise.allSettled<ModelAnswer>([
withTimeout(signal => callModel('fast-model', prompt, { signal }), 8_000),
withTimeout(signal => callModel('reasoning-model', prompt, { signal }), 15_000),
]);
const usableAnswers: ModelAnswer[] = results.flatMap(result =>
result.status === 'fulfilled' ? [result.value] : [],
);
Sample answer: In an AI product I do not treat all async calls the same. If the next step needs the previous result, I keep the flow sequential. If I am calling independent providers or retrieval sources, I run them concurrently. For user-facing AI features, I usually prefer
allSettledbecause partial results are better than a blank screen. I also attach timeout and cancellation so stale requests do not keep updating the UI after the user has moved on.
Interviewer follow-ups
- If one task in Promise.all rejects, can you still get the values that already fulfilled? No.
allis fail-fast: once one rejects, the whole thing rejects and the fulfilled values are lost. UseallSettledinstead, or wrap each task with.catchso it resolves to a sentinel value beforeall. - After you call AbortController.abort(), what happens to the in-flight await? For fetch it throws an
AbortError. But abort only signals the underlying layer; if your code never reads the signal, theawaitkeeps running. You have to thread the signal all the way down and check it inside your own tasks. - Several models run in parallel — how do you decide which answer to use? Do not just race on whoever returns first. Balance quality against latency: let a fast model produce a placeholder, then swap in the reasoning model when it lands; or run both and pick by a validation rule or confidence score.
My take
When interviewers ask about Promise, they are not really testing API names — they want to see if you can match a concurrency strategy to a product situation. I have met plenty of people who recite the all / allSettled difference fluently but freeze at "what happens to the old request after the user navigates away." Cancellation and timeout are the traps you hit every day in production. One more thing people forget: more concurrency is not always better. Model providers rate-limit you, so firing a raw Promise.all at a dozen endpoints gets you a wall of 429s. A concurrency pool with throttling is the real answer.
2. Event loop #JavaScript 48%
One-line answer: The event loop runs one macrotask, drains all microtasks, gives the browser a chance to render, and then moves to the next macrotask.
- Macrotasks include timers, user events, network callbacks, and script execution.
- Microtasks include Promise callbacks,
queueMicrotask, and MutationObserver. - Too many microtasks can starve rendering and make streaming output feel frozen.
- For long AI text rendering, batch updates and yield back to the browser.
Sample answer: For streaming UI, I avoid setting React state for every token. Even though each update feels small, the microtask queue can grow quickly and block painting. I usually buffer chunks, flush on
requestAnimationFrame, and keep rendering work under the frame budget.
Interviewer follow-ups
- Why is a for-loop full of await slow, and can you rewrite it with Promise.all? Sequential await makes each task wait for the previous one, so total time is the sum. If the tasks are independent, collect them into an array first and use
all/allSettled— total time becomes the max instead of the sum. Confirm independence before doing it. - Which runs first, queueMicrotask or setTimeout(0)? Microtasks always drain right after the current macrotask and before the next one, so
queueMicrotaskwins.setTimeout(0)also has a roughly 4ms minimum-delay clamp in most browsers. - Where does requestAnimationFrame fit in the event loop? It is not part of the standard macro/micro queues. It is driven by the render cadence and fires before each paint, so flushing a stream on rAF naturally aligns with frames and stays smoother than setTimeout.
My take
Everyone can recite the event loop definition. The thing that actually separates a senior candidate is "microtasks starve rendering." I once shipped a streaming Markdown renderer that set state on every token; long answers froze the page, and only chunking plus an rAF flush fixed it. If you can say "I hit this bug and here is how I fixed it," that beats memorizing the definition every time.
3. Garbage collection and memory leaks #JavaScript 53%
One-line answer: JavaScript garbage collection is reachability-based. Objects reachable from roots stay alive; unreachable objects can be collected.
- Common leaks: unremoved event listeners, timers, detached DOM nodes, growing caches, and stale closures.
- AI apps add new leak risks: long conversations, streaming buffers, large Markdown ASTs, cached embeddings, and WebSocket listeners.
- Use Chrome heap snapshots, allocation timeline, and route-by-route reproduction to find objects that only grow.
type ConversationId = string;
type StreamAnswerOptions = {
conversationId: ConversationId;
signal: AbortSignal;
};
useEffect(() => {
const controller = new AbortController();
const handleResize = (): void => measureLayout();
window.addEventListener('resize', handleResize);
void streamAnswer({ conversationId, signal: controller.signal } satisfies StreamAnswerOptions);
return () => {
controller.abort();
window.removeEventListener('resize', handleResize);
};
}, [conversationId]);
Interviewer follow-ups
- How do you actually track down a memory leak, step by step? Chrome DevTools Memory: take a heap snapshot, run a round of interaction (send ten messages, say), take another snapshot, and compare the delta. Or open the allocation timeline and watch which object types only grow. You are hunting for "monotonically increasing."
- A long chat gets slower the longer it runs — what is leaking? Full history kept in memory, unflushed stream buffers, cached Markdown ASTs, stale closures, AbortControllers and listeners never released, or a global store that is never trimmed.
- Do WeakMap / WeakSet prevent leaks? They just stop the key from blocking GC, which makes them fine for associated caches. They are not a cleanup silver bullet — if you still strongly reference the data somewhere else, it leaks all the same.
My take
Memory leaks are sneakier in AI apps than in classic frontends because long sessions are the norm. My rule is to bind every stream, listener, timer, and controller to the lifecycle of the conversation or component it belongs to, and abort plus remove them all when the session switches or the component unmounts. One easy miss: dumping the whole conversation into a global store and never trimming it. The store gets heavy over time, and re-renders get slower with it.
CSS, Layout, and Rendering
1. Positioning and stacking context #CSS 75%
One-line answer: Positioning decides where an element is laid out. Stacking context decides how elements are layered on the z-axis.
relativeoffsets the element visually but keeps its original space.absolutepositions against the nearest positioned ancestor.fixedpositions against the viewport unless a transformed ancestor changes the containing block.stickybehaves like relative until it reaches the threshold, then behaves like fixed inside its scroll container.- New stacking contexts can come from
position + z-index,transform,opacity < 1,filter, andisolation.
Sample answer: When a dropdown or tooltip is hidden behind another layer, I do not blindly raise
z-index. I first check whether an ancestor created a new stacking context. In complex apps I usually put overlays into a portal, define a small z-index scale, and avoid local z-index wars.
Interviewer follow-ups
- Why is z-index 9999 still hidden behind another layer? Because an ancestor created a new stacking context, and z-index is only compared within the same context. Walk up the ancestor chain instead of cranking the number higher.
- Which properties create a new stacking context?
positionnon-static with a z-index,transform,opacitybelow 1,filter,will-change,isolation: isolate, andmix-blend-mode. That list is your checklist when an overlay is clipped. - Common reasons sticky stops working? Usually a parent with
overflow: hidden/autoconstrains the sticky scroll context, or an ancestor is not tall enough to give it room to stick.
My take
This question almost always gets pushed into stacking context. Memorizing z-index values is useless; you have to understand that z-index is scoped. The engineering answer is a portal plus a fixed z-index scale (toast > modal > popover > content) so individual components never invent their own z-index. Otherwise they fight each other eventually.
2. Reflow, repaint, and composite #CSS 48%
One-line answer: Layout changes trigger reflow, visual changes trigger repaint, and transform/opacity can often be handled by the compositor.
- Changing width, height, font size, or DOM structure can trigger layout.
- Changing color or shadow usually triggers paint.
- Animating
transformandopacityis usually cheaper than animating layout properties. will-changecan help, but overusing it wastes memory.
.message-enter {
opacity: 0;
transform: translateY(8px);
}
.message-enter-active {
opacity: 1;
transform: translateY(0);
transition: opacity 160ms ease, transform 160ms ease;
}
Interviewer follow-ups
- What happens if you alternate reads and writes of offsetWidth? Forced synchronous layout, or layout thrashing. The browser is forced to recompute layout repeatedly because you read it and then mutate it. Fix it by batching all reads, then all writes.
- How should will-change be used?Only for a property that is genuinely about to change a lot, and remove it when done. Leaving will-change on permanently keeps a compositor layer alive and burns memory.
- Why is animating transform/opacity cheaper than left/top?left/top trigger layout; transform and opacity usually only hit composite and go straight to the GPU without dropping frames.
My take
I tie this one straight back to streaming UI. A message list that keeps appending will reflow constantly if its enter/exit animations animate height or width. Swapping to transform and opacity with a FLIP approach, plus content-visibility: auto to skip off-screen rendering, is what keeps a long conversation smooth.
3. Responsive layout #CSS
Sample answer: I prefer layout primitives before media-query patches. For example, use flexible grid, fluid widths, and sensible min/max constraints first. Then use breakpoints for real product changes, such as moving a side panel under the main content or collapsing a full toolbar into an action menu.
Interviewer follow-ups
- Container queries versus media queries — when does each win?Media queries respond to the viewport; container queries respond to a component's own container. Reusable components that have to look right at any width are the container-query use case.
- How do you handle responsive without the layout jumping on resize?Fluid units (clamp, min/max, vw), intrinsic sizing (min-content, fit-content), and a few real breakpoints instead of pixel-perfect ones for every device.
My take
I reach for layout primitives before media queries. A fluid grid plus clamp-based sizing handles most of the range by itself; breakpoints then handle genuine product changes, like collapsing a toolbar. If you find yourself writing a breakpoint per device, the primitives are doing too little work.
React Interview Core
1. State management: Redux versus Zustand #React 70%
One-line answer: Local UI state stays local. Shared client state can go to Zustand or Redux. Server state should usually be handled by a data-fetching cache instead of being copied into global state.
- Use component state for modal state, temporary input, and one-screen interactions.
- Use Zustand for lightweight global client state with low ceremony.
- Use Redux when the team needs strict conventions, debugging, middleware, or a large shared domain model.
- Use React Query/SWR-style tools for server cache, invalidation, retries, and background refetch.
Sample answer: For an AI chat product, I would keep the current input and streaming draft local or in a feature store. I would not put every token into Redux. Conversation metadata can be global, while historical messages should be paginated and cached. The important thing is to separate UI state, server state, and long-lived product state.
Interviewer follow-ups
- Where do the streaming tokens go, and why not Redux?Dispatching an action per token means high-frequency store writes plus re-renders on every subscribed component — a disaster. Keep them in local state or a feature store and flush in batches per frame.
- Can React Query replace a global state library?For server state, yes. But UI state (modals, filters, drafts) still needs a home. Plenty of people assume RQ handles everything and end up stuffing UI state wherever it fits.
- How do you avoid unnecessary re-renders with Zustand?Subscribe to a specific slice with a selector instead of the whole store. Otherwise every store change re-renders every component that touches it.
My take
The way I think about state management, "which library" is the secondary question and "which kind of state goes where" is the primary one. Three buckets: ephemeral UI state, cached server state, long-lived product state. That classification is what interviewers want to hear, not a Redux-versus-Zustand API comparison. My default is Zustand plus React Query, and I only reach for Redux on large teams that need strict conventions and middleware.
2. React performance optimization #React 52%
One-line answer: React performance work is mostly about reducing unnecessary renders, reducing expensive work during render, and shrinking the amount of DOM that needs to update.
- Use
memoonly when props are stable and the component is expensive enough. - Use
useMemofor expensive derived values, not for every object. - Use
useCallbackwhen function identity affects memoized children or effect dependencies. - For long lists, use virtualization before micro-optimizing child components.
type Message = {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
};
type MessageListProps = {
messages: Message[];
};
const MessageList = memo(function MessageList({ messages }: MessageListProps): JSX.Element {
return (
<>
{messages.map(message => (
<MessageItem key={message.id} message={message} />
))}
</>
);
});
const visibleMessages = useMemo<Message[]>(
() => filterMessages(messages, keyword),
[messages, keyword],
);
Interviewer follow-ups
- You added memo but nothing got faster — why?Three possibilities: props change identity every render (functions or objects without useCallback/useMemo), the component is not expensive enough for memo to pay off, or the bottleneck is not render at all but network or heavy computation.
- What happens if you overuse useMemo?Every render pays to compute the dependency array and store the cache, which costs memory. Wrapping a cheap value in useMemo can make things slower.
- What does React 18 concurrent rendering change for performance work?
useTransitionanduseDeferredValuedefer low-priority updates so a big list or form input stops blocking typing. But under concurrent mode you have to be careful about side-effect timing.
My take
The biggest misconception in React performance is "memo everything." Profile first with the React DevTools Profiler to find which components re-render and why, then fix that specifically. I prefer reducing load at the architecture level — virtualization, splitting heavy components, moving derivation into useMemo or a worker — over sprinkling memo across every component.
3. Fiber and scheduling #React 55%
Sample answer: Fiber turns React rendering work into units that can be paused, resumed, and prioritized. That is what makes concurrent rendering possible. In interview terms, I explain it as React moving from one big recursive render to a cooperative scheduler with a tree of work units.
Interviewer follow-ups
- Why was Fiber needed, and what was wrong with recursive rendering?Recursive rendering could not be interrupted, so a large component tree held the main thread for too long and blocked interaction. Fiber splits the work into units that can pause and yield between frames.
- How is time-slicing triggered?The scheduler checks after each work unit whether the time budget (about 5ms) is up; if so it yields the main thread and resumes on the next frame.
- useEffect versus useLayoutEffect in this scheduling model?Layout effects run synchronously after DOM mutation and block paint; plain effects run asynchronously after paint. Use a layout effect only when you must measure layout and adjust styles in the same frame.
My take
The three words that answer this are "interruptible, resumable, prioritizable." The bonus is explaining why interruptibility matters — a big list diff cannot lock the main thread. One layer deeper: React 18's concurrent mode is built on top of this scheduling, which is exactly why useTransition is possible.
Network and Security
1. Cookie, session, token, and JWT #Network 78%
One-line answer: Cookie is a browser storage and transport mechanism. Session is server-side login state. Token is a credential. JWT is a signed token format that can carry claims.
- Cookie is automatically sent by the browser, so it needs
HttpOnly,Secure, andSameSite. - Session is easy to revoke because the server owns the state.
- JWT is stateless and scalable, but revocation and rotation need careful design.
- Access tokens should be short-lived. Refresh tokens need stricter protection.
Interviewer follow-ups
- localStorage or Cookie for a JWT?localStorage is convenient but exposed to XSS (any JS can read it). An HttpOnly Cookie resists theft by XSS but is vulnerable to CSRF, so it needs SameSite. No silver bullet — it depends on your threat model.
- How do you force a JWT to log out?A stateless JWT cannot be revoked by itself. You keep a server-side blocklist (which makes it stateful again), use short access tokens plus refresh-token rotation, or rotate the signing key to invalidate everything.
- What is refresh-token rotation?Every refresh issues a new access token and a new refresh token, and invalidates the old refresh token. Even a stolen refresh token stays valid only briefly.
My take
The point I push here is that statelessness is not free. JWT's selling point is no server state and easy scaling, and the price is hard revocation. I have seen teams adopt JWT by default, only to discover they cannot do "force logout" or "invalidate on password change." My preference is short access tokens plus a refresh token in an HttpOnly Cookie, paired with a server-revocable session. More robust than pure JWT.
2. XSS, CSRF, and AI content injection #Security 59%
One-line answer: XSS runs attacker-controlled code in the user’s browser. CSRF tricks the browser into sending an authenticated request. Prompt injection tricks the model or agent into ignoring the intended instruction boundary.
- For Markdown from an LLM, parse to an AST and sanitize with an allowlist before rendering.
- Do not render model output with raw
innerHTMLunless it has been sanitized. - For tool-calling agents, treat model output as untrusted input, not as authority.
- For destructive tools, require permission checks, argument validation, and confirmation.
Sample answer: In a classic frontend app, XSS is already dangerous. In an AI app it is worse because the output may look like normal generated content. I would sanitize rich text, isolate previews where possible, and make tool execution go through a server-side policy layer. The model can suggest an action, but it should not be allowed to execute arbitrary actions directly.
Interviewer follow-ups
- The model emits Markdown containing a script tag — how does your render pipeline stop it?Parse to an AST, sanitize with a DOMPurify allowlist, then render. Never trust model output with a raw innerHTML, even when it looks like normal content.
- What is the real difference between prompt injection and XSS?XSS executes malicious code in the browser; prompt injection makes the model follow the wrong instructions. One is code injection, the other is semantic injection, and the defenses are completely different — sanitize for XSS, separate data from instructions for injection.
- How do you protect an agent that calls tools?Model output is input, not authority. Tool execution goes through server-side argument validation, permission checks, and scope limits, with human confirmation for destructive actions. The model can only suggest; it cannot execute.
My take
Security is badly underweighted in AI apps. We at least know how to defend classic XSS. AI adds another layer: model output looks like "normal generated content," and an attacker can hide instructions inside retrieved content or user input. My rule is one sentence: the model can only ever suggest, never execute. Execution authority lives in a server-side policy layer, and the frontend never trusts model output directly.
Browser Performance
1. Performance metrics #Performance 70%
One-line answer: Performance should be measured from the user’s experience, not just from bundle size or average API latency.
- FCP: when the first content is painted.
- LCP: when the main content becomes visible.
- INP: whether interactions respond quickly.
- CLS: whether the page jumps while loading.
- TTFB: whether the server starts responding quickly.
Interviewer follow-ups
- What did INP replace, and why?In 2024 INP replaced FID as a Core Web Vital. FID measured only the first interaction; INP measures the slowest interaction across the whole page lifecycle, which tracks real experience far better.
- How do you chase a high CLS?Find unsized images, ad slots, and late-loading fonts that shift content. Give media explicit dimensions, reserve placeholders, and avoid inserting content above what is already visible.
- What drives LCP?Usually the load time of the largest image or text block. Levers are preload, CDN, modern image formats, SSR, and inlining critical CSS for the first viewport.
My take
The big trap in performance metrics is "look at the average, ignore the tail." An average LCP of 2s with a P75 of 5s is just as bad an experience. Google's thresholds are all on P75. In monitoring I always watch the median and the P75 / P95 together — the average alone hides the tail where the real pain lives.
2. Virtual scrolling #Performance 78%
One-line answer: Virtual scrolling keeps the full data set in memory but only renders the visible rows plus a small overscan area.
- Fixed-height rows are simpler: index can be calculated from scroll offset.
- Dynamic-height rows need measurement, caching, and correction after render.
- Keep keyboard navigation, screen readers, and scroll restoration in mind.
- For chat messages, appending, prepending history, and preserving scroll position are the hard parts.
type VisibleRangeInput = {
scrollTop: number;
rowHeight: number;
viewportHeight: number;
total: number;
overscan?: number;
};
type VisibleRange = {
start: number;
end: number;
};
function getVisibleRange({
scrollTop,
rowHeight,
viewportHeight,
total,
overscan = 5,
}: VisibleRangeInput): VisibleRange {
const firstVisible = Math.floor(scrollTop / rowHeight);
const lastVisible = Math.ceil((scrollTop + viewportHeight) / rowHeight);
return {
start: Math.max(0, firstVisible - overscan),
end: Math.min(total, lastVisible + overscan),
};
}
Interviewer follow-ups
- What if row heights are not fixed?You have to measure and cache each row height, then correct with a ResizeObserver after render. Much harder than the fixed case, and the total height jumps while scrolling until it settles.
- What is special about virtual scrolling in a chat?Chat is inverted — history is prepended above, and you must preserve the user's current scroll position when it lands. During streaming append you also have to detect whether the user is near the bottom and only auto-follow then, otherwise you yank them away from the history they were reading.
- How do you keep keyboard nav and accessibility working?Use
aria-rowcount/aria-rowindexand make sure focus can move to rows that are currently virtualized out. Most libraries do this poorly, so expect to patch it yourself.
My take
The deepest hole I have fallen into with virtual scrolling is the inverted chat case. The instant history is prepended, if total height changes and you do not compensate, the message the user was reading gets shoved out of view. The other frequent pain is "the stream steals the scrollbar" — the user scrolls up to read history while the model keeps appending, and auto-scrolling to the bottom is maddening. Detecting "is the user near the bottom" before deciding to follow is the whole game for UX.
3. Web Worker #Performance 62%
Sample answer: I use a worker when the work is CPU-heavy and does not need direct DOM access: token counting, parsing a huge Markdown document, syntax highlighting, compression, or local embedding calculations. The tradeoff is serialization cost, so I would not move tiny tasks into a worker just for the sake of it.
Interviewer follow-ups
- Why is main-thread/worker communication expensive?Data has to be structured-cloned (or transferred), and large objects are costly to serialize. For small tasks a worker is actually slower than just running on the main thread.
- Why is token counting a good worker candidate?Libraries like tiktoken walk the string and run BPE — pure CPU, tens to hundreds of milliseconds. On the main thread it blocks typing.
- What does SharedArrayBuffer buy you?Shared memory with no copy. But it requires COOP / COEP cross-origin isolation headers, which is a deployment hurdle, so it is not free to adopt.
My take
More workers is not better. My bar is: the task takes longer than a frame (about 16ms) and does not need frequent DOM access. Token counting, large Markdown parsing, and local embedding are the textbook cases. Always price in the communication cost, or you end up slower than just computing on the main thread because of all the data you shipped back and forth.
Engineering, Build, and Observability
1. Frontend observability #Engineering 95%
One-line answer: A frontend observability system should connect user behavior, errors, performance, API calls, release version, and business context into one traceable timeline.
- Error: JS errors, unhandled rejections, resource load failures, and React error boundaries.
- Performance: Core Web Vitals, route timing, API latency, long tasks, and memory signals.
- Behavior: page views, clicks, form steps, feature usage, and funnel drop-off.
- Context: user segment, route, device, browser, release version, experiment group, and trace id.
- Pipeline: SDK collection → batching → sampling → upload → server normalization → storage → dashboard and alerting.
Sample answer: I would design the SDK first around reliability and low overhead. It should batch events, retry carefully, avoid blocking unload, support sampling, and always attach release and route context. For AI features I would also log request id, model provider, latency, token usage, cancellation, and whether the answer was streamed or retried.
Interviewer follow-ups
- The page is about to close — how do you keep the last few events from being lost?Use
navigator.sendBeacon(it does not block unload) orfetchwithkeepalive. A plain fetch or XHR gets cancelled by the browser during unload and drops data. - How does the monitoring SDK avoid slowing down the product?Batching, sampling, sending on idle (
requestIdleCallback), and failing soft. Everything inside the SDK is wrapped in try / catch so a monitoring bug never takes the page down with it. - How do you link one request across frontend and backend?The frontend generates or forwards a traceId, the BFF propagates it to every downstream service, and all logs and metrics key off that id to form a distributed trace.
My take
Observability shows up the most in interviews (95%), and most people answer it too vaguely with a list of categories. The way to stand out is to call out the metrics unique to AI business: not just JS errors and Web Vitals, but model provider, token spend, streaming time-to-first-token, whether the request was cancelled or retried, and the user's thumbs-up / thumbs-down. Those are observability dimensions traditional monitoring does not cover at all.
2. Vite versus Webpack #Engineering
One-line answer: Vite is faster in development because it serves native ESM and transforms files on demand. Webpack builds a full dependency graph and is still powerful for complex legacy build pipelines.
Interviewer follow-ups
- Why is Vite fast in dev but still bundles for production?In dev it serves native ESM on demand with no bundling, which is why it is fast. In production it still bundles with Rollup for tree-shaking, minification, and long-term cache hits.
- Where is Webpack still the better choice?Legacy projects with heavily customized loaders or plugins, Module Federation across multiple apps, and complex multi-entry enterprise builds. The Vite ecosystem has not fully covered those cases.
My take
Pure trivia — answering the dev-versus-prod difference is enough. My view is Vite is the default for new projects and Webpack is legacy baggage. But do not rewrite a working build just for the sake of it; price in the migration effort and the risk before you touch a project that already runs fine.
3. Canary release and rollback #Engineering 40%
Sample answer: I separate deployment from release. Deployment ships code to production; release controls who can see it. A safe rollout needs feature flags, a small first cohort, metrics, error budgets, automatic rollback signals, and a clean way to turn off the feature without redeploying.
Interviewer follow-ups
- What is the real difference between deployment and release?Deployment puts code on production machines; release decides who can see it. Splitting the two is the foundation of safe releases — when something goes wrong you can stop the release without rolling back the deployment.
- How does rollback happen automatically?Set an error budget and threshold on key metrics (error rate, latency, SLO); when the budget is consumed, automatically cut back to the previous release without a redeploy.
- What makes a feature flag well-designed?Independent of code deployment, able to target by cohort, percentage, or environment, with a kill switch for instant shutdown and an audit log. An instant kill beats a redeploy rollback.
My take
I stress "release is independent of deployment." AI features especially need gradual rollout, because model behavior is uncertain in a way a normal feature like a button is not. My cadence is internal dogfood first, then 1% → 10% → full, watching metrics at each step. An instant kill switch matters more than a rollback redeploy — when something breaks, those few minutes are the difference between an incident and a non-event.
Algorithm and Hand-Written Coding
1. Promise concurrency limit #Coding 70%
One-line answer: A concurrency pool keeps at most N tasks running. When one finishes, the next task starts.
type AsyncTask<T> = () => Promise<T>;
async function limitConcurrency<T>(
tasks: Array<AsyncTask<T>>,
limit: number,
): Promise<T[]> {
const results: T[] = new Array(tasks.length);
let nextIndex = 0;
async function worker(): Promise<void> {
while (nextIndex < tasks.length) {
const currentIndex = nextIndex++;
results[currentIndex] = await tasks[currentIndex]();
}
}
const workerCount = Math.min(limit, tasks.length);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return results;
}
Sample answer: I use this pattern when calling many APIs, uploading files, generating thumbnails, or running model requests with rate limits. The key is to preserve result order while controlling the number of active tasks.
Interviewer follow-ups
- Why a worker model instead of recursing inside then?Each worker loops and pulls tasks, which keeps the concurrency count exact and the error handling uniform. Recursing inside then tends to produce a version where the concurrency count drifts and errors get swallowed.
- How do you preserve result order?Write into a pre-allocated results array keyed by task index — do not push. Push gives you completion order, not task order.
- What if one task fails?Depends on the requirement: for all-or-nothing, rethrow inside the worker and let
Promise.allreject; for partial success, catch the error into a sentinel value written back at that index.
My take
This is a high-frequency coding question (70%) and you must be able to write it from memory. I use the worker model because it decouples "concurrency control" from "task pulling," which is the least error-prone shape. In the interview I proactively call out two details: results written by index for ordering, and workerCount as min(limit, tasks.length). Those two edge cases are usually what the interviewer is listening for, and nailing them beats just getting the body right.
2. Debounce and throttle #Coding 67%
One-line answer: Debounce waits until calls stop. Throttle guarantees execution at most once in a time window.
type AnyFunction = (...args: never[]) => void;
function debounce<T extends AnyFunction>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timerId: ReturnType<typeof setTimeout> | undefined;
return (...args: Parameters<T>): void => {
if (timerId !== undefined) clearTimeout(timerId);
timerId = setTimeout(() => fn(...args), delay);
};
}
function throttle<T extends AnyFunction>(fn: T, interval: number): (...args: Parameters<T>) => void {
let lastRunAt = 0;
return (...args: Parameters<T>): void => {
const now = Date.now();
if (now - lastRunAt < interval) return;
lastRunAt = now;
fn(...args);
};
}
Interviewer follow-ups
- What are debounce's leading and trailing options?Trailing (the default) fires once after calls stop; leading fires immediately on the first call. A search box wants trailing; a "prevent double-submit" button may want leading.
- Your throttle is the timestamp version — what is it missing?If the final call lands inside the window it never runs, so the trailing call is lost. A complete version adds a trailing call to guarantee the final invocation.
- Can requestAnimationFrame act as a throttle?Yes — it throttles by frame, which is ideal for scroll and resize because it naturally aligns with paint.
My take
Debounce and throttle are also must-memorize. I drop in a mention of lodash's leading / trailing / maxWait options to show I have thought about the edges, not just written a version that runs. Being able to name the scenario for each is enough: search box uses debounce, scroll-loading uses throttle, drag and animation use rAF.
AI and LLM Frontend
1. Streaming rendering and real-time Markdown #AI 84%
One-line answer: Streaming UI should separate transport, decoding, buffering, parsing, rendering, and cancellation. Do not tie every network chunk directly to a React render.
- Transport: SSE, fetch stream, or WebSocket.
- Decode:
TextDecoderwith streaming mode. - Buffer: collect incomplete chunks and flush on a safe schedule.
- Parse: support incomplete Markdown without breaking code blocks and tables.
- Render: batch updates, sanitize HTML, and keep scroll behavior predictable.
type RenderScheduler = (markdown: string) => void;
async function readStreamingResponse(
response: Response,
scheduleRender: RenderScheduler,
): Promise<void> {
if (!response.body) throw new Error('Response body is not readable.');
const decoder = new TextDecoder();
let buffer = '';
for await (const chunk of response.body as ReadableStream<Uint8Array>) {
buffer += decoder.decode(chunk, { stream: true });
scheduleRender(buffer);
}
buffer += decoder.decode();
scheduleRender(buffer);
}
Sample answer: The main mistake is thinking streaming is only a network problem. In practice it is also a rendering problem. I would buffer chunks, render at frame-friendly intervals, handle incomplete Markdown, and make cancellation first-class. Otherwise the UI gets janky or stale responses keep writing into the page.
Interviewer follow-ups
- A code block is only half-output (the closing fence has not arrived) — what does your Markdown renderer do?Be tolerant: detect the unclosed fence, close it temporarily for the render pass, and do not let the rest of the text render as code or blow up the layout.
- The user navigated away but the old stream is still writing to the UI — what now?Give every request an id, abort the old one when a new request starts, and before applying an update check whether the current response id is still the latest — otherwise drop it. This is your race-condition guard.
- Why can't you setState on every chunk?High-frequency setState triggers re-render after re-render, the microtask queue piles up, the main thread saturates, and the UI freezes. You need buffering plus a per-frame flush.
My take
Streaming rendering is the single most important AI-frontend question (84%). I lean on two points that get skipped most often: cancellation is first-class, not an afterthought; and incomplete Markdown has to be tolerated. Get either wrong and the demo looks fine while the real long answers break. I have personally hit "the stale response races over the new one" — the user asks two questions back to back, the slow one lands last and overwrites the fast answer. So request-id checking is now standard in every streaming module I write.
2. SSE versus WebSocket #AI 71%
One-line answer: SSE is a good default for one-way model output. WebSocket is better for bidirectional, low-latency, long-lived interaction.
| Choice | Best for | Tradeoff |
|---|---|---|
| SSE | Streaming assistant answers, simple server-to-client updates | One-way only, text-based |
| WebSocket | Realtime collaboration, voice, agent status, bidirectional control | More stateful, more operational complexity |
| Fetch stream | Simple streaming over normal HTTP | Browser and proxy behaviour needs testing |
Interviewer follow-ups
- Does SSE reconnect on its own?The native
EventSourceauto-reconnects and can resume withLast-Event-ID. A fetch stream does not — you implement reconnect and resume yourself. - SSE gets buffered by Nginx or a proxy — what do you do?Disable proxy buffering (set the
X-Accel-Buffering: noresponse header), setContent-Type: text/event-stream, and make sure the server flushes. Otherwise streaming degrades into one batched response. - When do you actually need WebSocket?Bidirectional, low-latency, realtime collaboration, or voice. One-way model output is fine over SSE — do not reach for WebSocket to look modern and then carry the extra operational weight for nothing.
My take
My real-world preference is SSE or fetch stream first, because one-way model output does not need a bidirectional channel at all. I have watched teams adopt WebSocket because it felt more modern and then inherit connection management, heartbeats, reconnect logic, and state sync as pure extra burden. Simple beats complex unless there is a concrete realtime-collaboration requirement.
3. Token counting and context budgeting #AI 77%
One-line answer: Token count is model-tokenizer dependent. Frontend should estimate or calculate tokens before sending long prompts, files, or conversation history.
- Use the target model’s tokenizer when accuracy matters.
- Keep separate budgets for system prompt, history, retrieved context, user input, and expected output.
- Summarize or trim older messages instead of blindly sending the whole conversation.
- Show the user when content is too long and explain what will be omitted.
Interviewer follow-ups
- How does the frontend estimate tokens, and what if it is inaccurate?Use a library like tiktoken against the target model's tokenizer. When the estimate is unreliable, be conservative — over-count and trigger trimming sooner rather than under-counting and hitting a hard error.
- The conversation history is too long — what do you do?Sliding window, summarization, or keeping only the last few turns plus the system prompt. Never send the whole history blindly.
- How do you split the budget across system, retrieval, and user input?Bucketed: system is fixed, retrieval is trimmable, history gets summarized, user input is preserved, and output is reserved. When over budget, cut by priority — retrieval and old history first.
My take
The hard part of token budgeting is not counting tokens — it is the product decision of "what happens when we are over." My approach is bucketing plus transparency: when over budget, tell the user "this is long, so earlier conversation was omitted" instead of silently truncating. Silent truncation makes the model suddenly lose memory and leaves the user confused. Making the cut visible is more professional than pretending nothing happened.
4. RAG and citations #AI 75%
One-line answer: RAG is not just vector search. A reliable RAG system needs chunking, indexing, retrieval, reranking, context assembly, generation, and citation mapping.
- Chunk by meaning, not only by fixed length.
- Store metadata: source, title, section, timestamp, permissions, and chunk id.
- Use hybrid retrieval when exact keywords matter.
- Rerank before generation to reduce noisy context.
- Map generated claims back to source chunks for citations.
Sample answer: For an enterprise AI frontend, I would make citations part of the data model, not a decorative UI layer. The frontend should know which answer span came from which source chunk. If the answer cannot be traced, the UI should lower confidence instead of pretending it is grounded.
Interviewer follow-ups
- Why is vector search alone not enough?Vectors recall things that are semantically close but actually wrong, and they are bad at proper nouns, IDs, and exact keyword matches. So you go hybrid (vector plus BM25) and then rerank.
- How should chunks be cut?By meaning and by heading, not by a fixed character count. Leave overlap to preserve context. Keep tables and code as whole blocks and never cut through them.
- How do you align citations with the answer?Have the model tag each span with its source during generation, or post-process by matching answer sentences to source chunks. When something cannot be matched, lower confidence or label it "no source found."
My take
My core belief with RAG is "traceability beats looking accurate." The frontend has to know which source each answer span came from, and when it cannot trace one, it should honestly say "no source found" instead of pretending it is grounded. In an enterprise setting, a confidently fabricated answer is far more dangerous than an honest "I'm not sure" — the first sends a user off to act on wrong information, the second just makes the model look less capable.
5. Prompt injection and tool calling #AI
Sample answer: I treat prompt injection as an instruction-boundary problem. Retrieved content, user input, and tool results are data. They should not be able to override system policy. For tool calling, I validate arguments server-side, check permissions, limit tool scope, and require confirmation for destructive actions.
Interviewer follow-ups
- How do you separate instructions from data in the prompt?Mark retrieved and user-supplied content as untrusted data inside clear delimiters, keep system policy in a privileged section the model is told never to override, and never let data segments issue commands.
- How do you rate-limit what an agent can actually do?Least-privilege tool scopes, per-user and per-session action quotas, a human-in-the-loop gate for anything destructive, and full audit logging of every tool call.
My take
Prompt injection is the security problem most AI demos quietly ignore. My position is that the model only ever suggests actions; the server validates, authorizes, and executes. If you let model output directly drive destructive tools, one injected instruction inside a retrieved document is all it takes. The defense is not a better prompt — it is a real permission boundary on the execution side.
Architecture and System Design
1. Designing an AI chat application #Architecture 77%
One-line answer: A solid AI chat architecture separates UI, conversation state, model orchestration, retrieval, tool execution, safety policy, and observability.
- Frontend: message rendering, streaming state, cancellation, file upload, citation UI, and error recovery.
- BFF: auth, request normalization, quota, model routing, and response streaming.
- Orchestrator: prompt assembly, retrieval, tool calls, retries, and fallback.
- Storage: conversations, message versions, attachments, embeddings, and audit logs.
- Observability: latency, token usage, cost, model errors, user feedback, and tool-call traces.
Sample answer: I would not put provider-specific details directly into the frontend. I would hide them behind a BFF and a model adapter layer. That gives us room to switch models, add fallback, enforce policy, and attach consistent tracing without rewriting the UI.
Interviewer follow-ups
- How does the frontend handle multiple models or providers?A BFF plus a model adapter layer normalizes streaming, errors, tool calls, and billing into one contract for the frontend. The frontend never talks to a provider SDK directly.
- How do you control cost under high concurrency?Per-user or per-tenant quota and rate limiting, caching common queries, model routing by question difficulty (cheap model for easy ones), and a hard token-budget cap.
- What version info do messages need to store?At minimum a message id, version number, the model and params used to generate it, and the cited chunk ids — enough to trace and regenerate.
My take
In system-design questions I keep coming back to the BFF layer. It is not a dumb forwarder — it absorbs provider differences, auth, rate limiting, billing, and observability all at once. The frontend gets a product-shaped API and stays unaware when a model swaps behind it. Design that layer well and swapping providers, adding fallback, or rolling out gradually all stay easy; design it badly and every new model means a frontend rewrite.
2. BFF and schema adaptation #Architecture 70%
Sample answer: A BFF is useful when the frontend needs a product-shaped API rather than raw backend service shapes. For AI products, it is especially useful because each model provider streams, errors, bills, and formats tool calls differently. The BFF can normalize those differences into one frontend contract.
Interviewer follow-ups
- Can the BFF become a bottleneck or single point of failure?Yes, so keep it stateless, horizontally scalable, and cached. It should do orchestration and adaptation only — push heavy computation down into dedicated services.
- What is the difference between a BFF and a gateway?A gateway is a general entry point handling auth, rate limiting, and routing; a BFF is an adaptation layer for a specific frontend, close to the product shape. They coexist — gateway in front, BFF behind.
My take
The thing I make clear on BFF is its "adaptation" essence: give the frontend a product-shaped API and hide the backend service shapes. AI needs it badly because provider differences are huge. The cautionary tale is the reverse drift — do not let the BFF slowly accumulate business logic until it becomes a second backend. Once that happens, maintenance cost spikes. Keep it thin.
3. State machine #Architecture 55%
One-line answer: A state machine makes async UI predictable by defining valid states and transitions explicitly.
type ChatState = 'idle' | 'streaming' | 'cancelled' | 'failed' | 'done';
type ChatEvent = 'submit' | 'chunk' | 'cancel' | 'error' | 'done' | 'retry' | 'reset' | 'regenerate';
const transitions: Record<ChatState, ChatEvent[]> = {
idle: ['submit'],
streaming: ['chunk', 'cancel', 'error', 'done'],
cancelled: ['retry', 'reset'],
failed: ['retry', 'reset'],
done: ['regenerate', 'reset'],
};
function canTransition(state: ChatState, event: ChatEvent): boolean {
return transitions[state].includes(event);
}
Interviewer follow-ups
- Why a state machine instead of a few booleans?A few booleans (isLoading + isError + isDone) admit illegal combinations like "loading and done at once." A state machine uses enumerated states and legal transitions to make those illegal combinations impossible by construction.
- How does state move when a stream is cancelled mid-flight?streaming goes to cancelled via cancel, and cancelled can only retry back to streaming or reset to idle. That way a cancelled state can never accept a chunk event, which prevents dirty updates.
- Is a library like XState necessary?For simple cases, an enum plus a transition table is enough. Reach for XState when states multiply, side effects get complex, and you need visualization and testability.
My take
My experience with state machines: the more complex the async UI, the more they pay off. An AI streaming chat has five states — idle, streaming, cancelled, failed, done — and a few booleans will inevitably produce an illegal combination. One transition table is clearer than a pile of ifs. But do not pull in a heavy library for a simple case; an enum and a table solve most of them.
HR and Soft-Skill Scripts
HR rounds do not drill into implementation details the way technical rounds do, but they push in two directions: every claim you make has to survive a request for a concrete example, and they are testing whether the direction you describe is something you are genuinely investing in. Each question below has a "where they dig deeper" block and a "my take."
1. What is your core strength? #HR
Sample answer: My strongest advantage is that I can connect product experience, frontend architecture, and AI implementation. I am not only thinking about whether the model can answer. I also care about streaming UX, cancellation, observability, permissions, cost, and failure recovery. That helps me turn an AI demo into something closer to a real product.
Where they dig deeper
- "Can you give a concrete example of those abilities?"You need one or two STAR examples: which project, what you did, and the result in numbers. Stopping at slogans collapses the answer.
- "Compared to a pure algorithm or AI engineer, what is your weak spot?"Admit it honestly (model training and fine-tuning are not my home ground), then frame your positioning as engineering delivery that complements the algorithm team rather than pretending to be a generalist.
My take
HR questions are the easiest to answer vaguely. My rule is that every claim has to be followed immediately by a concrete example. A strength is not a stack of adjectives; it is "I did X, and the result was Y." Admitting a weak spot honestly is itself a plus — interviewers have seen too much packaging, and someone genuine with self-awareness is rare.
2. Why AI frontend / agent engineering?
Sample answer: Frontend used to be mostly about screens and interactions. AI products add a new layer: the interface is now connected to model behaviour, context, tools, and user trust. I like that intersection. It fits my frontend background, but it also pushes me into systems thinking.
Where they dig deeper
- "What have you been learning recently?"Name something real and recent — an agent framework, a paper, a side project. This is the litmus test for whether you are actually investing in the direction.
- "Where do you think the ceiling is for this direction?"Show a real opinion: tooling, observability, and reliability are underserved compared to model capability itself, and that gap is the opportunity.
My take
What the interviewer is really measuring on a motivation question is whether you are chasing a trend. I anchor on "it builds on my frontend background and pushes me toward systems thinking," which frames it as a natural extension rather than an impulsive pivot. The key is a ready answer to "what have you been learning recently" — name something from the last week or two and your authenticity is established on the spot.
3. How do you explain a career gap?
Sample answer: I had a period where I stepped back and reset my direction. I used that time to rebuild my focus around AI tooling, frontend architecture, and full-stack product work. I am now looking for a role where I can apply that direction seriously, not just return to the same kind of frontend work I did before.
Where they dig deeper
- "What did you produce during that time?"Even a side project, open-source contribution, technical writing, or systematic study counts — anything visible that proves the period was not empty.
- "Why come back now?"Frame the gap as an active redirect with concrete learning and output behind it, and say you are ready to commit to the new direction now, not that you were struggling to find work.
My take
The worst thing you can do on a gap question is hedge. I frame it openly as an active choice: I stepped back to redirect toward AI, I have concrete learning and output from that time, and now I am ready to commit. Honest plus visible output equals credible. Do not over-explain — make the point in a sentence or two and steer back to "here is what I can do now."
4. What should a strong closing answer sound like?
Sample answer: What I can bring is not just React experience. I can help build AI features with a production mindset: streaming UI, safe tool calls, retrieval UX, monitoring, performance, and clean architecture. That is the direction I want to grow in, and it is where I think I can create the most value.
Points to land at the close
- "Do you have any questions for us?"Do not ask a nothing question. Ask something with teeth: what is the team's biggest technical challenge right now, how is AI feature reliability measured, what does the release and rollout process look like. A good question is itself a plus.
- The one-line positioning.Sum up with "I am not just React — I can turn an AI feature into a real product," and pin the production-mindset differentiator into the interviewer's memory.
My take
The close is the last chance to leave a "production mindset" impression. I pin the positioning in one sentence, then hand the floor back and ask one or two sharp questions — not just courtesy, but a signal that I am genuinely evaluating whether this team is worth joining. The quality of your questions often reveals your real level more than your answers do.
