- Published on
Senior Frontend Engineer Interview: The Complete Guide to Key Questions & Expert Answers
Senior Frontend Engineer Interview: The Complete Guide to Key Questions & Expert Answers
Preparing for a senior frontend engineer interview requires mastering not just coding skills, but also system design thinking, architectural patterns, and deep technical knowledge. This comprehensive guide covers the essential questions and model answers that will help you excel in your next interview. Whether you're interviewing at a startup or a big tech company, these topics form the foundation of senior-level technical discussions. 🚀
Table of Contents
- System Design & Architecture
- Performance & Debugging
- Testing Strategy
- Web Performance Optimization
- Accessibility (A11Y)
- CI/CD & DevOps
- State Management
- Code Quality & Collaboration
- Modern Stack & Advanced Topics
System Design & Architecture
1. How would you build a reusable component library for multiple products?
Building a scalable component library requires careful planning and architectural thinking. Here's how I approach this challenge:
Atomic Design Methodology
I follow the atomic design principles to create a logical hierarchy:
- Atoms: The smallest building blocks (buttons, inputs, colors, typography tokens)
- Molecules: Combinations of atoms (search input + submit button, form field + label)
- Organisms: Groups of molecules forming distinct UI sections (navigation bar, product card grid)
- Templates: Page-level structure with generic layout components
- Pages: Templates populated with real content and data
Component Classification Strategy
// Example structure
src/
├── tokens/ # Design tokens (colors, spacing, typography)
├── atoms/ # Basic building blocks
│ ├── Button/
│ ├── Input/
│ └── Typography/
├── molecules/ # Composed components
│ ├── SearchBox/
│ ├── FormField/
│ └── ProductCard/
├── organisms/ # Complex UI sections
│ ├── Header/
│ ├── ProductGrid/
│ └── Footer/
└── templates/ # Layout templates
├── PageLayout/
└── DashboardLayout/
Technical Implementation
Tooling Stack:
- Storybook for isolated component development and documentation
- TypeScript for strong typing and prop inference
- CSS-in-JS or Tailwind CSS for styling
- Jest + Testing Library for component testing
Monorepo Structure:
{
"name": "@company/design-system",
"workspaces": ["packages/tokens", "packages/components", "packages/icons", "packages/utils"]
}
Versioning & Distribution:
- Follow semantic versioning (semver)
- Use Changeset for automated versioning
- Publish to private npm registry
- Maintain comprehensive changelog
- Provide migration guides for breaking changes
Theming System
// Design tokens approach
export const tokens = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a'
},
semantic: {
error: '#ef4444',
warning: '#f59e0b',
success: '#10b981'
}
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px'
}
}
// CSS custom properties for runtime theming
:root {
--color-primary-500: #3b82f6;
--spacing-md: 16px;
}
[data-theme="dark"] {
--color-primary-500: #60a5fa;
}
2. How would you design a real-time dashboard with multiple widgets?
Real-time dashboards present unique challenges around state management, performance, and data synchronization.
Architecture Overview
// State management with Redux Toolkit + WebSocket middleware
const store = configureStore({
reducer: {
dashboard: dashboardSlice.reducer,
widgets: widgetSlice.reducer,
websocket: websocketSlice.reducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(websocketMiddleware),
})
// WebSocket middleware for real-time updates
const websocketMiddleware: Middleware = (store) => (next) => (action) => {
if (action.type.startsWith('websocket/')) {
// Handle WebSocket connection and message routing
websocketManager.send(action.payload)
}
return next(action)
}
Widget System Design
// Widget interface
interface Widget {
id: string
type: 'chart' | 'metric' | 'table' | 'map'
config: WidgetConfig
layout: { x: number; y: number; w: number; h: number }
dataSource: string
refreshInterval: number
}
// Widget component with performance optimization
const Widget = React.memo(({ widget }: { widget: Widget }) => {
const data = useSelector(selectWidgetData(widget.id))
const isVisible = useIntersectionObserver(widget.id)
// Only update when widget is visible
useEffect(() => {
if (isVisible && widget.refreshInterval > 0) {
const interval = setInterval(() => {
dispatch(fetchWidgetData(widget.id))
}, widget.refreshInterval)
return () => clearInterval(interval)
}
}, [isVisible, widget.refreshInterval])
return (
<WidgetContainer>
<WidgetRenderer type={widget.type} data={data} config={widget.config} />
</WidgetContainer>
)
})
Performance Optimizations
- Virtual scrolling for large datasets
- Intersection Observer to pause updates for off-screen widgets
- RequestIdleCallback for non-critical updates
- Web Workers for heavy data processing
- Memoization of expensive calculations
Performance & Debugging
3. How do you debug a page that lags while scrolling?
Scroll performance issues are common in complex applications. Here's my systematic approach:
Performance Profiling
Chrome DevTools Performance Tab:
// Record performance during scroll // Look for: // - Long tasks (>50ms) // - Layout thrashing // - Excessive paint operations // - JavaScript execution blocking main thread
Identify Performance Bottlenecks:
- Check for forced synchronous layouts
- Look for expensive CSS properties (box-shadow, gradients)
- Identify JavaScript operations running on scroll
Common Solutions
GPU Acceleration:
/* Use transform and opacity for animations */
.smooth-animation {
transform: translateX(0);
transition: transform 0.3s ease-out;
will-change: transform; /* Hint to browser for optimization */
}
/* Avoid animating layout properties */
.avoid-this {
transition: left 0.3s ease-out; /* Triggers layout */
}
Scroll Optimization:
// Passive event listeners
window.addEventListener('scroll', handleScroll, { passive: true })
// Throttle scroll handlers
const throttledScrollHandler = throttle(handleScroll, 16) // ~60fps
// Use requestAnimationFrame for smooth updates
const handleScroll = () => {
requestAnimationFrame(() => {
// Update UI here
})
}
Virtual Scrolling for Large Lists:
import { FixedSizeList as List } from 'react-window'
const VirtualizedList = ({ items }) => (
<List height={400} itemCount={items.length} itemSize={50} itemData={items}>
{({ index, style, data }) => (
<div style={style}>
<ListItem item={data[index]} />
</div>
)}
</List>
)
CSS Containment
.widget-container {
contain: layout style paint;
/* Isolates the widget's rendering from affecting other elements */
}
Testing Strategy
4. What is your testing strategy for a large-scale frontend project?
A comprehensive testing strategy ensures code quality and prevents regressions as the application scales.
Testing Pyramid
// 1. Unit Tests (70%) - Fast, isolated, numerous
// Test pure functions, hooks, utilities
describe('calculateTotal', () => {
it('should calculate total with tax correctly', () => {
expect(calculateTotal(100, 0.1)).toBe(110)
})
})
// Custom hook testing
const { result } = renderHook(() => useCounter(0))
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
// 2. Integration Tests (20%) - Component interactions
// Test component behavior with React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
test('user can submit form successfully', async () => {
render(<ContactForm onSubmit={mockSubmit} />)
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'test@example.com' },
})
fireEvent.click(screen.getByRole('button', { name: /submit/i }))
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
})
})
})
// 3. End-to-End Tests (10%) - Critical user journeys
// Cypress example
describe('User Authentication Flow', () => {
it('should allow user to login and access dashboard', () => {
cy.visit('/login')
cy.get('[data-cy=email]').type('user@example.com')
cy.get('[data-cy=password]').type('password123')
cy.get('[data-cy=submit]').click()
cy.url().should('include', '/dashboard')
cy.get('[data-cy=welcome-message]').should('be.visible')
})
})
API Mocking Strategy
// MSW (Mock Service Worker) for API mocking
import { rest } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json([{ id: 1, name: 'John Doe', email: 'john@example.com' }]))
}),
rest.post('/api/users', (req, res, ctx) => {
return res(ctx.status(201), ctx.json({ id: 2, ...req.body }))
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
Accessibility Testing
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
test('should not have accessibility violations', async () => {
const { container } = render(<App />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
Web Performance Optimization
5. What performance optimization techniques have you applied?
Performance optimization is crucial for user experience and business metrics.
Bundle Optimization
// Code splitting with React.lazy
const Dashboard = React.lazy(() => import('./Dashboard'))
const Profile = React.lazy(() => import('./Profile'))
const App = () => (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
)
// Dynamic imports for heavy libraries
const loadChartLibrary = async () => {
const { Chart } = await import('chart.js')
return Chart
}
Webpack Bundle Analysis:
{
"scripts": {
"analyze": "npm run build && npx webpack-bundle-analyzer build/static/js/*.js"
}
}
Library Optimization
// Replace heavy libraries
// Before: moment.js (67kb)
import moment from 'moment'
// After: date-fns (2kb per function)
import { format, addDays } from 'date-fns'
// Tree shaking with proper imports
// Good
import { debounce } from 'lodash-es'
// Bad (imports entire library)
import _ from 'lodash'
Image Optimization
// Modern image formats with fallback
<picture>
<source srcSet="image.avif" type="image/avif" />
<source srcSet="image.webp" type="image/webp" />
<img src="image.jpg" alt="Description" loading="lazy" />
</picture>
// Responsive images
<img
src="small.jpg"
srcSet="small.jpg 480w, medium.jpg 800w, large.jpg 1200w"
sizes="(max-width: 480px) 480px, (max-width: 800px) 800px, 1200px"
alt="Responsive image"
loading="lazy"
/>
Runtime Performance
// Debouncing expensive operations
const debouncedSearch = useMemo(() => debounce(searchAPI, 300), [])
// Using requestIdleCallback for non-critical work
const processNonCriticalData = (data) => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Process data when browser is idle
updateAnalytics(data)
})
} else {
// Fallback for older browsers
setTimeout(() => updateAnalytics(data), 0)
}
}
// Web Workers for heavy computations
const worker = new Worker('/heavy-computation-worker.js')
worker.postMessage({ data: largeDataset })
worker.onmessage = (event) => {
setProcessedData(event.data)
}
Advanced Performance Optimization Techniques
RequestIdleCallback for Non-Critical Updates
requestIdleCallback
allows you to schedule work during the browser's idle periods, ensuring smooth user interactions by avoiding blocking the main thread during critical rendering.
// Enhanced requestIdleCallback implementation
class IdleWorkScheduler {
private workQueue: Array<() => void> = []
private isProcessing = false
scheduleWork(task: () => void, priority: 'low' | 'high' = 'low') {
if (priority === 'high') {
this.workQueue.unshift(task)
} else {
this.workQueue.push(task)
}
if (!this.isProcessing) {
this.processWork()
}
}
private processWork() {
if (this.workQueue.length === 0) {
this.isProcessing = false
return
}
this.isProcessing = true
if ('requestIdleCallback' in window) {
requestIdleCallback(
(deadline) => {
// Process as many tasks as possible during idle time
while (deadline.timeRemaining() > 0 && this.workQueue.length > 0) {
const task = this.workQueue.shift()
task?.()
}
// Schedule next batch if there are remaining tasks
if (this.workQueue.length > 0) {
this.processWork()
} else {
this.isProcessing = false
}
},
{ timeout: 1000 }
) // Fallback timeout
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(() => {
const task = this.workQueue.shift()
task?.()
this.processWork()
}, 0)
}
}
}
// Usage in React components
const useIdleWork = () => {
const scheduler = useRef(new IdleWorkScheduler())
const scheduleIdleWork = useCallback((task: () => void, priority?: 'low' | 'high') => {
scheduler.current.scheduleWork(task, priority)
}, [])
return scheduleIdleWork
}
// Real-world example: Analytics and logging
const AnalyticsComponent = () => {
const scheduleIdleWork = useIdleWork()
const trackUserInteraction = useCallback(
(event: AnalyticsEvent) => {
// Critical: Update UI immediately
setLocalState(event)
// Non-critical: Send analytics during idle time
scheduleIdleWork(() => {
analytics.track(event)
localStorage.setItem('lastInteraction', JSON.stringify(event))
updateUserPreferences(event)
})
},
[scheduleIdleWork]
)
return (
<button onClick={() => trackUserInteraction({ type: 'button_click', target: 'cta' })}>
Click Me
</button>
)
}
GPU Acceleration Optimization
GPU acceleration moves rendering work from the CPU to the GPU, dramatically improving performance for animations and visual effects.
/* GPU Acceleration Best Practices */
/* ✅ GOOD: Properties that trigger GPU acceleration */
.gpu-optimized {
/* Transform properties (most efficient) */
transform: translateX(100px) translateY(50px) translateZ(0);
/* Opacity changes */
opacity: 0.8;
/* 3D transforms automatically trigger GPU layers */
transform: rotateX(45deg) rotateY(30deg);
/* Filter effects */
filter: blur(5px) brightness(1.2);
/* Will-change hint (use sparingly) */
will-change: transform, opacity;
}
/* ❌ BAD: Properties that force layout/paint on CPU */
.cpu-expensive {
/* Layout properties (avoid animating these) */
width: 200px;
height: 100px;
left: 100px;
top: 50px;
/* Paint properties (expensive) */
background-color: red;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
/* Advanced GPU optimization techniques */
.advanced-gpu-layer {
/* Force layer creation for complex animations */
transform: translateZ(0); /* or translate3d(0,0,0) */
/* Isolate animation layers */
isolation: isolate;
/* Optimize for animations */
backface-visibility: hidden;
perspective: 1000px;
}
/* Layer management for performance */
.animation-container {
/* Create stacking context to control layers */
position: relative;
z-index: 0;
}
.floating-element {
/* Promote to its own layer */
position: absolute;
transform: translateZ(0);
/* Smooth animations */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Memory-efficient animations */
@keyframes slideIn {
from {
transform: translateX(-100%) translateZ(0);
opacity: 0;
}
to {
transform: translateX(0) translateZ(0);
opacity: 1;
}
}
.slide-animation {
animation: slideIn 0.3s ease-out forwards;
/* Remove will-change after animation */
animation-fill-mode: both;
}
// JavaScript GPU acceleration helpers
class GPUAnimationManager {
private activeAnimations = new Set<string>()
// Optimize element for animation
prepareForAnimation(element: HTMLElement, properties: string[] = ['transform', 'opacity']) {
// Add will-change hint
element.style.willChange = properties.join(', ')
// Force layer creation if needed
if (!element.style.transform) {
element.style.transform = 'translateZ(0)'
}
}
// Clean up after animation
cleanupAfterAnimation(element: HTMLElement) {
// Remove will-change to free up GPU memory
element.style.willChange = 'auto'
// Remove unnecessary transforms
if (element.style.transform === 'translateZ(0)') {
element.style.transform = ''
}
}
// Smooth animation with requestAnimationFrame
animateElement(
element: HTMLElement,
from: Record<string, number>,
to: Record<string, number>,
duration: number = 300
): Promise<void> {
return new Promise((resolve) => {
const startTime = performance.now()
const animationId = `${element.id}-${Date.now()}`
this.activeAnimations.add(animationId)
this.prepareForAnimation(element, Object.keys(to))
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// Easing function (ease-out)
const eased = 1 - Math.pow(1 - progress, 3)
// Apply transformations
Object.keys(to).forEach((property) => {
const start = from[property] || 0
const end = to[property]
const current = start + (end - start) * eased
if (property === 'translateX' || property === 'translateY') {
element.style.transform = `${property}(${current}px) translateZ(0)`
} else if (property === 'opacity') {
element.style.opacity = current.toString()
}
})
if (progress < 1 && this.activeAnimations.has(animationId)) {
requestAnimationFrame(animate)
} else {
this.activeAnimations.delete(animationId)
this.cleanupAfterAnimation(element)
resolve()
}
}
requestAnimationFrame(animate)
})
}
}
// Usage example
const animationManager = new GPUAnimationManager()
const slideInElement = async (element: HTMLElement) => {
await animationManager.animateElement(
element,
{ translateX: -100, opacity: 0 },
{ translateX: 0, opacity: 1 },
300
)
}
CSS Containment for Performance Isolation
CSS Containment isolates parts of the page to prevent layout, paint, and style recalculations from propagating throughout the document.
/* CSS Containment Types */
/* Layout Containment - isolates layout calculations */
.widget-container {
contain: layout;
/* Changes inside this container won't affect layout outside */
}
/* Style Containment - isolates style recalculations */
.theme-container {
contain: style;
/* CSS custom property changes won't propagate outside */
}
/* Paint Containment - isolates painting operations */
.complex-graphics {
contain: paint;
/* Painting inside won't affect elements outside */
overflow: hidden; /* Required for paint containment */
}
/* Size Containment - fixes element size */
.fixed-size-widget {
contain: size;
/* Element size doesn't depend on children */
width: 300px;
height: 200px;
}
/* Combined Containment - maximum performance isolation */
.performance-critical {
contain: layout style paint;
/* Or use shorthand: contain: strict; (includes size) */
}
/* Real-world examples */
/* Dashboard widgets that update independently */
.dashboard-widget {
contain: layout style paint;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 8px;
}
/* Chat messages that don't affect each other */
.chat-message {
contain: layout paint;
padding: 8px 12px;
margin: 4px 0;
border-radius: 12px;
}
/* Product cards in a grid */
.product-card {
contain: layout paint;
/* Prevents price updates from affecting other cards */
}
/* Modal dialogs */
.modal-content {
contain: style;
/* Theme changes inside modal don't leak out */
}
/* Performance monitoring containment */
.performance-container {
contain: strict; /* layout + style + paint + size */
/* Monitor containment effectiveness */
performance-observer: layout-shift;
}
// JavaScript containment management
class ContainmentManager {
// Dynamically apply containment based on content
optimizeElement(element: HTMLElement, contentType: string) {
switch (contentType) {
case 'widget':
// Independent widgets that update frequently
element.style.contain = 'layout style paint'
break
case 'list-item':
// List items that change independently
element.style.contain = 'layout paint'
break
case 'modal':
// Modals with different styling
element.style.contain = 'style'
break
case 'animation':
// Elements with complex animations
element.style.contain = 'layout paint'
element.style.willChange = 'transform'
break
}
}
// Monitor containment performance
measureContainmentImpact(element: HTMLElement) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
entries.forEach((entry) => {
if (entry.entryType === 'layout-shift') {
console.log('Layout shift in contained element:', entry)
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
return () => observer.disconnect()
}
}
// React hook for automatic containment
const useContainment = (elementRef: RefObject<HTMLElement>, contentType: string) => {
useEffect(() => {
if (!elementRef.current) return
const manager = new ContainmentManager()
manager.optimizeElement(elementRef.current, contentType)
const cleanup = manager.measureContainmentImpact(elementRef.current)
return cleanup
}, [elementRef, contentType])
}
// Usage in components
const OptimizedWidget = ({ data }) => {
const widgetRef = useRef<HTMLDivElement>(null)
useContainment(widgetRef, 'widget')
return (
<div ref={widgetRef} className="widget-container">
{/* Widget content that updates frequently */}
<WidgetContent data={data} />
</div>
)
}
RequestAnimationFrame for Smooth Updates
requestAnimationFrame
synchronizes updates with the browser's refresh rate for smooth animations and prevents blocking the main thread.
// Advanced RequestAnimationFrame patterns
class SmoothAnimationController {
private animationId: number | null = null
private isRunning = false
// Smooth scrolling with requestAnimationFrame
smoothScrollTo(element: HTMLElement, targetScrollTop: number, duration: number = 500) {
const startScrollTop = element.scrollTop
const distance = targetScrollTop - startScrollTop
const startTime = performance.now()
const animateScroll = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// Easing function for smooth animation
const easeInOutCubic =
progress < 0.5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2
element.scrollTop = startScrollTop + distance * easeInOutCubic
if (progress < 1) {
this.animationId = requestAnimationFrame(animateScroll)
} else {
this.animationId = null
}
}
this.stop() // Cancel any existing animation
this.animationId = requestAnimationFrame(animateScroll)
}
// Smooth value transitions
animateValue(
from: number,
to: number,
duration: number,
onUpdate: (value: number) => void,
onComplete?: () => void
) {
const startTime = performance.now()
const distance = to - from
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
// Smooth easing
const eased = 1 - Math.pow(1 - progress, 3)
const currentValue = from + distance * eased
onUpdate(currentValue)
if (progress < 1) {
this.animationId = requestAnimationFrame(animate)
} else {
this.animationId = null
onComplete?.()
}
}
this.stop()
this.animationId = requestAnimationFrame(animate)
}
// Performance-optimized scroll handler
createSmoothScrollHandler(callback: (scrollY: number) => void) {
let ticking = false
return () => {
if (!ticking) {
requestAnimationFrame(() => {
callback(window.scrollY)
ticking = false
})
ticking = true
}
}
}
stop() {
if (this.animationId) {
cancelAnimationFrame(this.animationId)
this.animationId = null
}
}
}
// React hooks for smooth animations
const useSmoothAnimation = () => {
const controllerRef = useRef(new SmoothAnimationController())
useEffect(() => {
return () => controllerRef.current.stop()
}, [])
return controllerRef.current
}
// Smooth counter animation
const useAnimatedCounter = (targetValue: number, duration: number = 1000) => {
const [displayValue, setDisplayValue] = useState(0)
const animation = useSmoothAnimation()
useEffect(() => {
animation.animateValue(displayValue, targetValue, duration, setDisplayValue)
}, [targetValue, duration])
return Math.round(displayValue)
}
// Performance-optimized scroll effects
const useParallaxEffect = (speed: number = 0.5) => {
const elementRef = useRef<HTMLElement>(null)
const animation = useSmoothAnimation()
useEffect(() => {
if (!elementRef.current) return
const handleScroll = animation.createSmoothScrollHandler((scrollY) => {
if (elementRef.current) {
const offset = scrollY * speed
elementRef.current.style.transform = `translateY(${offset}px) translateZ(0)`
}
})
window.addEventListener('scroll', handleScroll, { passive: true })
return () => window.removeEventListener('scroll', handleScroll)
}, [speed])
return elementRef
}
// Usage examples
const AnimatedComponents = () => {
const animation = useSmoothAnimation()
const counterValue = useAnimatedCounter(1234, 2000)
const parallaxRef = useParallaxEffect(0.3)
const handleSmoothScroll = () => {
const target = document.getElementById('target-section')
if (target) {
animation.smoothScrollTo(document.documentElement, target.offsetTop)
}
}
return (
<div>
<div>Counter: {counterValue}</div>
<div ref={parallaxRef}>Parallax Element</div>
<button onClick={handleSmoothScroll}>Smooth Scroll</button>
</div>
)
}
Performance Monitoring and Debugging
// Performance monitoring for optimization techniques
class PerformanceMonitor {
// Monitor GPU layer count
static checkLayerCount() {
// Enable in Chrome DevTools: Rendering > Layer borders
console.log('Enable Chrome DevTools > Rendering > Layer borders to visualize GPU layers')
}
// Monitor main thread blocking
static measureMainThreadBlocking(taskName: string, task: () => void) {
const start = performance.now()
task()
const duration = performance.now() - start
if (duration > 16.67) {
// More than one frame at 60fps
console.warn(`${taskName} blocked main thread for ${duration.toFixed(2)}ms`)
}
}
// Monitor containment effectiveness
static observeLayoutShifts(element: HTMLElement) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'layout-shift' && entry.value > 0.1) {
console.warn('Significant layout shift detected:', entry)
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
return () => observer.disconnect()
}
}
These optimization techniques work together to create highly performant web applications. The key is understanding when and how to apply each technique based on your specific performance bottlenecks.
Resource Loading Optimization
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/primary.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/api/critical-data" as="fetch" crossorigin />
<!-- DNS prefetching for external resources -->
<link rel="dns-prefetch" href="//analytics.google.com" />
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<!-- Resource hints -->
<link rel="preconnect" href="//api.example.com" />
Accessibility (A11Y)
6. How would you make a custom dropdown accessible?
Creating accessible custom components requires understanding ARIA patterns and keyboard navigation.
Semantic HTML Structure
const AccessibleDropdown = ({ options, value, onChange, label }) => {
const [isOpen, setIsOpen] = useState(false)
const [activeIndex, setActiveIndex] = useState(-1)
const buttonRef = useRef(null)
const listRef = useRef(null)
const handleKeyDown = (event) => {
switch (event.key) {
case 'ArrowDown':
event.preventDefault()
if (!isOpen) {
setIsOpen(true)
} else {
setActiveIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0))
}
break
case 'ArrowUp':
event.preventDefault()
if (isOpen) {
setActiveIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1))
}
break
case 'Enter':
case ' ':
event.preventDefault()
if (isOpen && activeIndex >= 0) {
onChange(options[activeIndex])
setIsOpen(false)
buttonRef.current?.focus()
} else {
setIsOpen(!isOpen)
}
break
case 'Escape':
setIsOpen(false)
buttonRef.current?.focus()
break
}
}
return (
<div className="dropdown">
<label id={`${id}-label`} className="dropdown-label">
{label}
</label>
<button
ref={buttonRef}
type="button"
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-labelledby={`${id}-label`}
aria-activedescendant={
isOpen && activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined
}
onKeyDown={handleKeyDown}
onClick={() => setIsOpen(!isOpen)}
className="dropdown-button"
>
{value || 'Select an option'}
</button>
{isOpen && (
<ul ref={listRef} role="listbox" aria-labelledby={`${id}-label`} className="dropdown-list">
{options.map((option, index) => (
<li
key={option.value}
id={`${id}-option-${index}`}
role="option"
aria-selected={option.value === value}
className={`dropdown-option ${
index === activeIndex ? 'dropdown-option--active' : ''
}`}
onClick={() => {
onChange(option)
setIsOpen(false)
buttonRef.current?.focus()
}}
>
{option.label}
</li>
))}
</ul>
)}
</div>
)
}
Focus Management
// Focus trap for modal dropdowns
const useFocusTrap = (isActive: boolean, containerRef: RefObject<HTMLElement>) => {
useEffect(() => {
if (!isActive || !containerRef.current) return
const container = containerRef.current
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const firstElement = focusableElements[0] as HTMLElement
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement
const handleTabKey = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus()
e.preventDefault()
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus()
e.preventDefault()
}
}
}
container.addEventListener('keydown', handleTabKey)
firstElement?.focus()
return () => container.removeEventListener('keydown', handleTabKey)
}, [isActive, containerRef])
}
Screen Reader Testing
// Automated accessibility testing
import { axe } from 'jest-axe'
test('dropdown should be accessible', async () => {
const { container } = render(
<AccessibleDropdown
options={[{ value: '1', label: 'Option 1' }]}
onChange={jest.fn()}
label="Choose an option"
/>
)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
// Manual testing checklist:
// ✅ Screen reader announces role and state
// ✅ Keyboard navigation works correctly
// ✅ Focus is managed properly
// ✅ ARIA attributes are correct
// ✅ Color contrast meets WCAG AA standards
CI/CD & DevOps
7. What does your typical CI/CD pipeline look like?
A robust CI/CD pipeline ensures code quality and enables safe, frequent deployments.
GitHub Actions Pipeline
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type checking
run: npm run type-check
- name: Linting
run: npm run lint
- name: Unit tests
run: npm run test:coverage
- name: E2E tests
run: npm run test:e2e
- name: Build application
run: npm run build
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run security audit
run: npm audit --audit-level=moderate
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
deploy-preview:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
needs: [test, security]
steps:
- uses: actions/checkout@v3
- name: Deploy to Vercel Preview
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
deploy-production:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: [test, security]
steps:
- uses: actions/checkout@v3
- name: Deploy to production
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
vercel-args: '--prod'
Quality Gates
// package.json scripts
{
"scripts": {
"pre-commit": "lint-staged",
"prepare": "husky install",
"lint": "eslint src --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint src --ext .ts,.tsx,.js,.jsx --fix",
"type-check": "tsc --noEmit",
"test": "jest",
"test:coverage": "jest --coverage --watchAll=false",
"test:e2e": "playwright test"
}
}
// lint-staged configuration
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,md}": [
"prettier --write"
]
}
}
Environment Management
// Environment-specific configurations
const config = {
development: {
API_URL: 'http://localhost:3001',
DEBUG: true,
},
staging: {
API_URL: 'https://api-staging.example.com',
DEBUG: false,
},
production: {
API_URL: 'https://api.example.com',
DEBUG: false,
},
}
// Secure environment variable management
// Never commit sensitive data
// Use platform-specific secret management
State Management
8. When do you use Redux, Context, or Zustand?
Choosing the right state management solution depends on your application's complexity and requirements.
Decision Matrix
Use Case | Recommended Solution | Reasoning |
---|---|---|
Theme/Locale | React Context | Static global state, infrequent updates |
UI State | Zustand/useState | Simple, local to component tree |
Complex Business Logic | Redux Toolkit | Time-travel debugging, middleware support |
Server State | React Query/SWR | Caching, synchronization, optimistic updates |
React Context Implementation
// Good for: Theme, authentication, localization
const ThemeContext = createContext<{
theme: 'light' | 'dark'
toggleTheme: () => void
}>()
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const toggleTheme = useCallback(() => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
}, [])
const value = useMemo(
() => ({
theme,
toggleTheme,
}),
[theme, toggleTheme]
)
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}
// Custom hook for consuming context
export const useTheme = () => {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
Zustand Implementation
// Good for: Simple global state, UI state
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface CounterState {
count: number
increment: () => void
decrement: () => void
reset: () => void
}
export const useCounterStore = create<CounterState>()(
devtools(
persist(
(set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}),
{
name: 'counter-storage',
}
)
)
)
// Usage in component
const Counter = () => {
const { count, increment, decrement } = useCounterStore()
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}
Redux Toolkit Implementation
// Good for: Complex business logic, time-travel debugging
import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit'
// Async thunk for API calls
export const fetchUserProfile = createAsyncThunk(
'user/fetchProfile',
async (userId: string, { rejectWithValue }) => {
try {
const response = await userAPI.getProfile(userId)
return response.data
} catch (error) {
return rejectWithValue(error.response.data)
}
}
)
// Slice definition
const userSlice = createSlice({
name: 'user',
initialState: {
profile: null,
loading: false,
error: null,
},
reducers: {
clearError: (state) => {
state.error = null
},
updateProfile: (state, action) => {
state.profile = { ...state.profile, ...action.payload }
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUserProfile.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(fetchUserProfile.fulfilled, (state, action) => {
state.loading = false
state.profile = action.payload
})
.addCase(fetchUserProfile.rejected, (state, action) => {
state.loading = false
state.error = action.payload
})
},
})
export const { clearError, updateProfile } = userSlice.actions
export default userSlice.reducer
Server State with React Query
// Good for: API data, caching, synchronization
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
// Fetch user data with caching
export const useUser = (userId: string) => {
return useQuery({
queryKey: ['user', userId],
queryFn: () => userAPI.getProfile(userId),
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
})
}
// Mutation with optimistic updates
export const useUpdateUser = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: userAPI.updateProfile,
onMutate: async (newUserData) => {
// Cancel outgoing refetches
await queryClient.cancelQueries(['user', newUserData.id])
// Snapshot previous value
const previousUser = queryClient.getQueryData(['user', newUserData.id])
// Optimistically update
queryClient.setQueryData(['user', newUserData.id], newUserData)
return { previousUser }
},
onError: (err, newUserData, context) => {
// Rollback on error
queryClient.setQueryData(['user', newUserData.id], context.previousUser)
},
onSettled: (data, error, variables) => {
// Always refetch after error or success
queryClient.invalidateQueries(['user', variables.id])
},
})
}
Code Quality & Collaboration
9. How do you approach code reviews?
Code reviews are essential for maintaining code quality, knowledge sharing, and team collaboration.
Code Review Checklist
Functionality & Logic:
- ✅ Does the code solve the intended problem?
- ✅ Are edge cases handled appropriately?
- ✅ Is error handling comprehensive?
- ✅ Are there potential race conditions or memory leaks?
Code Quality:
// Good: Clear, descriptive naming
const calculateTotalPriceWithTax = (basePrice: number, taxRate: number): number => {
if (basePrice < 0 || taxRate < 0) {
throw new Error('Price and tax rate must be non-negative')
}
return basePrice * (1 + taxRate)
}
// Bad: Unclear naming and no error handling
const calc = (p, t) => p * (1 + t)
Performance Considerations:
// Review for performance anti-patterns
// Bad: Object creation in render
const Component = ({ items }) => (
<div>
{items.map((item) => (
<ItemComponent
key={item.id}
onClick={() => handleClick(item)} // New function on every render
style={{ color: 'red' }} // New object on every render
/>
))}
</div>
)
// Good: Memoized handlers and styles
const Component = ({ items }) => {
const handleClick = useCallback((item) => {
// Handle click
}, [])
const itemStyle = useMemo(() => ({ color: 'red' }), [])
return (
<div>
{items.map((item) => (
<ItemComponent key={item.id} onClick={() => handleClick(item)} style={itemStyle} />
))}
</div>
)
}
Review Process
Automated Checks First:
- Linting passes
- Type checking passes
- Tests pass
- Build succeeds
Manual Review Focus:
- Architecture and design patterns
- Security vulnerabilities
- Performance implications
- Maintainability
Constructive Feedback:
// Good review comment
❌ Consider extracting this complex logic into a separate function for better readability and testability.
```javascript
// Suggested refactor:
const validateUserInput = (input) => {
// validation logic here
}
```
// Bad review comment ❌ This code is bad.
#### **Knowledge Sharing**
```typescript
// Document complex decisions
/**
* Using WeakMap here to prevent memory leaks when components unmount.
* The browser will garbage collect the DOM nodes automatically,
* and our event listeners will be cleaned up as well.
*/
const componentListeners = new WeakMap()
// Share learning opportunities
// "TIL: React 18's automatic batching means we don't need to wrap
// multiple setState calls in unstable_batchedUpdates anymore"
Modern Stack & Advanced Topics
10. When would you choose React Server Components over Client Components?
React Server Components represent a fundamental shift in how we think about rendering and component boundaries.
Server Components Use Cases
// Good for: Static content, data fetching, SEO
// Server Component (runs on server)
async function ProductList() {
// This runs on the server
const products = await fetch('https://api.example.com/products').then((res) => res.json())
return (
<div>
<h1>Our Products</h1>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
// Benefits:
// ✅ Zero JavaScript sent to client for this component
// ✅ Direct access to backend resources
// ✅ Better SEO and initial page load
// ✅ Automatic code splitting
Client Components Use Cases
'use client' // This directive marks it as a Client Component
// Good for: Interactivity, browser APIs, state management
function AddToCartButton({ productId }: { productId: string }) {
const [isLoading, setIsLoading] = useState(false)
const [isAdded, setIsAdded] = useState(false)
const handleAddToCart = async () => {
setIsLoading(true)
try {
await addToCart(productId)
setIsAdded(true)
// Show success animation
} catch (error) {
// Handle error
} finally {
setIsLoading(false)
}
}
return (
<button
onClick={handleAddToCart}
disabled={isLoading}
className={`btn ${isAdded ? 'btn-success' : 'btn-primary'}`}
>
{isLoading ? 'Adding...' : isAdded ? 'Added!' : 'Add to Cart'}
</button>
)
}
// Benefits:
// ✅ Full interactivity
// ✅ Access to browser APIs
// ✅ State management
// ✅ Event handling
Hybrid Architecture
// Server Component that uses Client Components
async function ProductPage({ params }: { params: { id: string } }) {
// Server-side data fetching
const product = await getProduct(params.id)
const reviews = await getProductReviews(params.id)
return (
<main>
{/* Server-rendered content */}
<ProductInfo product={product} />
<ProductDescription description={product.description} />
{/* Client-side interactive components */}
<AddToCartButton productId={product.id} />
<ProductReviews reviews={reviews} />
<ShareButton productId={product.id} />
</main>
)
}
Performance Benefits
// Before: Traditional SPA (everything is client-side)
// Bundle size: ~500kb JavaScript
// Initial page load: Blank page → JavaScript loads → API calls → Content
// After: Server Components + Client Components
// Bundle size: ~150kb JavaScript (70% reduction)
// Initial page load: Immediate content → JavaScript hydrates interactive parts
// Real-world impact from migration:
// - 40% reduction in JavaScript bundle size
// - 60% improvement in First Contentful Paint
// - 35% improvement in Largest Contentful Paint
// - Better Core Web Vitals scores
Migration Strategy
// 1. Start with Server Components by default
// 2. Add 'use client' only when needed for:
// - useState, useEffect, or other hooks
// - Event handlers (onClick, onChange, etc.)
// - Browser-only APIs (localStorage, geolocation, etc.)
// - Custom hooks that use the above
// Migration checklist:
// ✅ Identify components that need interactivity
// ✅ Move state management to client boundaries
// ✅ Optimize data fetching patterns
// ✅ Test Server/Client component boundaries
Conclusion
Mastering these topics requires both theoretical knowledge and practical experience. Focus on understanding the underlying principles rather than memorizing specific syntax. During interviews, walk through your thought process, consider trade-offs, and don't hesitate to ask clarifying questions.
Remember that senior engineer interviews aren't just about technical knowledge—they're evaluating your ability to:
- Think systematically about complex problems
- Communicate technical concepts clearly
- Make informed trade-offs between different solutions
- Mentor and collaborate with team members
- Stay current with evolving best practices
The frontend landscape continues to evolve rapidly, but these fundamental concepts provide a solid foundation for tackling any modern web development challenge. Keep practicing, stay curious, and continue learning! 🚀