跪拜 Guibai
← Back to the summary

The Frontend Performance Playbook: From Core Web Vitals to Memory Leaks

Frontend Performance Optimization Interview Questions

Frontend performance optimization is a high-frequency topic in interviews. The following covers all aspects of performance optimization knowledge, including loading optimization, rendering optimization, network optimization, and metric measurement.


1. What are frontend performance metrics? How to measure them?

Core Web Vitals:

1. LCP (Largest Contentful Paint)
   - Measures loading performance: the time it takes for the largest element on the page to render.
   - Good: ≤ 2.5s | Needs Improvement: ≤ 4.0s | Poor: > 4.0s

2. INP (Interaction to Next Paint)
   - Measures interaction responsiveness: the time from user interaction to the page's response.
   - Good: ≤ 200ms | Needs Improvement: ≤ 500ms | Poor: > 500ms
   (Replaced the original FID)

3. CLS (Cumulative Layout Shift)
   - Measures visual stability: the degree to which page elements unexpectedly shift.
   - Good: ≤ 0.1 | Needs Improvement: ≤ 0.25 | Poor: > 0.25
// ========== Measuring with the Performance API ==========

// 1. Navigation Timing (time spent in each page load phase)
const timing = performance.getEntriesByType('navigation')[0]
console.log({
  DNSLookup: timing.domainLookupEnd - timing.domainLookupStart,
  TCPConnection: timing.connectEnd - timing.connectStart,
  TTFB: timing.responseStart - timing.requestStart,
  DOMParsing: timing.domInteractive - timing.responseEnd,
  DOMComplete: timing.domComplete - timing.domInteractive,
  PageFullyLoaded: timing.loadEventEnd - timing.fetchStart
})

// 2. Using PerformanceObserver to monitor performance metrics
// LCP
new PerformanceObserver((list) => {
  const entries = list.getEntries()
  const lastEntry = entries[entries.length - 1]
  console.log('LCP:', lastEntry.startTime, 'ms')
}).observe({ type: 'largest-contentful-paint', buffered: true })

// CLS
let clsScore = 0
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsScore += entry.value
    }
  }
  console.log('CLS:', clsScore)
}).observe({ type: 'layout-shift', buffered: true })

// FP / FCP
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.startTime}ms`)
  }
}).observe({ type: 'paint', buffered: true })

// 3. Using the web-vitals library (Google official)
import { onLCP, onINP, onCLS } from 'web-vitals'

onLCP((metric) => {
  console.log('LCP:', metric.value)
  // Report to monitoring platform
  sendToAnalytics({ name: 'LCP', value: metric.value })
})
onINP((metric) => console.log('INP:', metric.value))
onCLS((metric) => console.log('CLS:', metric.value))

Common Performance Metrics Overview:

Metric Full Name Meaning Good Value
FP First Paint First paint -
FCP First Contentful Paint First contentful paint ≤ 1.8s
LCP Largest Contentful Paint Largest contentful paint ≤ 2.5s
INP Interaction to Next Paint Interaction responsiveness ≤ 200ms
CLS Cumulative Layout Shift Layout shift ≤ 0.1
TTFB Time to First Byte Time to first byte ≤ 800ms
TTI Time to Interactive Time to interactive ≤ 3.8s

💡 Bonus interview point: Google replaced FID with INP as a Core Web Vital in 2024. FID only measured the first interaction delay, while INP measures the response delay of all interactions throughout the page's lifecycle.


2. What are the solutions for optimizing first-screen loading?

// ========== 1. Code Splitting + Route Lazy Loading ==========
// React
const Home = React.lazy(() => import('./pages/Home'))
const About = React.lazy(() => import('./pages/About'))

// Vue
const routes = [
  { path: '/', component: () => import('./views/Home.vue') },
  { path: '/about', component: () => import('./views/About.vue') }
]

// ========== 2. Preloading Critical Resources ==========
// <link rel="preload">: High-priority loading of resources needed for the current page
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/api/initial-data" as="fetch" crossorigin>

// <link rel="prefetch">: Low-priority loading of resources that might be needed in the future
<link rel="prefetch" href="/js/about-page.js">

// <link rel="preconnect">: Establish connections early
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

// ========== 3. Inlining Critical CSS ==========
// Inline the CSS needed for the first screen into the HTML, load the rest asynchronously
<style>
  /* Inline critical first-screen CSS directly */
  body { margin: 0; font-family: sans-serif; }
  .hero { height: 100vh; display: flex; align-items: center; }
</style>
<link rel="stylesheet" href="/styles/full.css" media="print" onload="this.media='all'">

// ========== 4. Skeleton Screens ==========
// Display the page structure outline before data loads to improve perceived speed
function SkeletonCard() {
  return (
    <div className="skeleton-card">
      <div className="skeleton-avatar animate-pulse" />
      <div className="skeleton-title animate-pulse" />
      <div className="skeleton-text animate-pulse" />
    </div>
  )
}

// ========== 5. SSR / SSG ==========
// SSR: Server-Side Rendering, returns complete HTML directly for the first screen
// SSG: Static Site Generation, generates HTML at build time

// Next.js SSG example
export async function getStaticProps() {
  const data = await fetchData()
  return { props: { data }, revalidate: 60 }  // ISR: Regenerate every 60 seconds
}

// ========== 6. Server Push (HTTP/2 Server Push) ==========
// The server actively pushes critical resources when responding with HTML
// Gradually being replaced by 103 Early Hints

3. Resource loading optimization strategies?

// ========== 1. Image Optimization ==========

// Use modern image formats
<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="fallback" loading="lazy">
</picture>

// Responsive images
<img
  srcset="image-320w.jpg 320w,
          image-640w.jpg 640w,
          image-1280w.jpg 1280w"
  sizes="(max-width: 600px) 320px,
         (max-width: 1200px) 640px,
         1280px"
  src="image-640w.jpg"
  alt="responsive image"
>

// Image lazy loading
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" alt="">

// Implementing lazy loading with IntersectionObserver
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src
      observer.unobserve(img)
    }
  })
}, { rootMargin: '200px' })  // Start loading 200px in advance

document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img))

// ========== 2. JavaScript Loading Optimization ==========
// defer: Asynchronous loading, executed in order after DOM parsing is complete (recommended)
<script defer src="main.js"></script>

// async: Asynchronous loading, executed immediately upon download (suitable for independent scripts like analytics)
<script async src="analytics.js"></script>

// Dynamic import + preloading
// Preload when the user's mouse enters the navigation
navLink.addEventListener('mouseenter', () => {
  import(/* webpackPrefetch: true */ './pages/About')
})

// ========== 3. Font Optimization ==========
// Preload critical fonts
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>

// Use font-display to avoid FOIT (Flash of Invisible Text)
@font-face {
  font-family: 'MyFont';
  src: url('/fonts/main.woff2') format('woff2');
  font-display: swap;  /* Use system font first, swap after font loads */
}

// Font subsetting: Only include the required characters
// Tools: fonttools/pyftsubset
// Reduces Chinese fonts from 10MB+ to a few hundred KB

// ========== 4. Third-Party Script Optimization ==========
// Load third-party scripts asynchronously
function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = src
    script.async = true
    script.onload = resolve
    script.onerror = reject
    document.body.appendChild(script)
  })
}

// Load non-critical third-party scripts when the browser is idle
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => loadScript('https://analytics.example.com/sdk.js'))
} else {
  setTimeout(() => loadScript('https://analytics.example.com/sdk.js'), 3000)
}

4. How to design an HTTP caching strategy?

Cache Priority:
Service Worker Cache → Memory Cache → Disk Cache → HTTP Request

HTTP caching is divided into two types:
1. Strong Cache: No request is sent, the cache is used directly.
2. Negotiated Cache: A request is sent to verify if the cache is valid.
# Nginx cache configuration example

# ===== HTML files: No cache (or short-term cache + negotiated cache) =====
location / {
    add_header Cache-Control "no-cache";  # Validate every time
    # or add_header Cache-Control "max-age=0, must-revalidate";
}

# ===== Static resources with hash: Long-term strong cache =====
location /assets/ {
    # Filename contains content hash (e.g., main.a1b2c3.js), hash changes only when content changes
    add_header Cache-Control "public, max-age=31536000, immutable";
    # immutable: Tells the browser this resource will never change
}

# ===== API responses: Choose based on scenario =====
location /api/ {
    # No cache
    add_header Cache-Control "no-store";

    # Or short-term cache
    # add_header Cache-Control "private, max-age=60";
}
// ========== Caching Strategy Best Practices ==========

// 1. HTML files → no-cache (negotiated cache)
//    Validate every time to ensure users get the latest version

// 2. JS/CSS/images (with contenthash) → strong cache for 1 year
//    Cache-Control: max-age=31536000, immutable
//    Content changes → hash changes → new URL → automatic update

// 3. API data → depends on the scenario
//    Real-time data: no-store (no caching)
//    Infrequently changing data: max-age=60 + stale-while-revalidate=300

// ========== Strong Cache vs. Negotiated Cache ==========
// Strong Cache (no request sent)
// Cache-Control: max-age=31536000
// → 200 (from disk cache) / (from memory cache)

// Negotiated Cache (request sent to validate)
// Request headers: If-None-Match: "abc123"  / If-Modified-Since: Wed, 21 Oct 2024
// Response headers: ETag: "abc123"          / Last-Modified: Wed, 21 Oct 2024
// → 304 Not Modified (cache is valid) or 200 (returns new content)

// ========== Service Worker Cache (Highest Priority) ==========
// Cache First strategy
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((cached) => {
      return cached || fetch(event.request).then((response) => {
        const clone = response.clone()
        caches.open('v1').then((cache) => cache.put(event.request, clone))
        return response
      })
    })
  )
})

Caching Strategy Decision Tree:

Resource Type Caching Strategy Cache-Control
HTML Negotiated Cache no-cache
JS/CSS (with hash) Long-term Strong Cache max-age=31536000, immutable
Images/Fonts (with hash) Long-term Strong Cache max-age=31536000
API (real-time data) No Cache no-store
API (infrequent changes) Short-term Cache max-age=60, stale-while-revalidate=300

5. What are the methods for rendering optimization?

// ========== 1. Reduce Reflow and Repaint ==========

// ❌ Frequent DOM manipulation triggers multiple reflows
for (let i = 0; i < 1000; i++) {
  document.body.appendChild(document.createElement('div'))
}

// ✅ Use DocumentFragment for batch operations
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
  fragment.appendChild(document.createElement('div'))
}
document.body.appendChild(fragment)  // Triggers only one reflow

// ✅ Use CSS class instead of multiple style modifications
// ❌ Triggers 3 reflows
element.style.width = '100px'
element.style.height = '200px'
element.style.margin = '10px'
// ✅ Triggers only 1 reflow
element.className = 'new-style'

// ========== 2. Use CSS Composite Properties ==========
// Only triggers compositing, not reflow or repaint (GPU accelerated)
.animated-element {
  /* ✅ transform and opacity only trigger the compositing layer */
  transform: translateX(100px);
  opacity: 0.5;
  will-change: transform;  /* Inform the browser in advance */

  /* ❌ The following properties trigger reflow */
  /* left: 100px; top: 50px; width: 200px; */
}

// ========== 3. Virtual List ==========
// Only renders elements within the visible area, solving long list performance issues
function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0)

  const startIndex = Math.floor(scrollTop / itemHeight)
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  )
  const visibleItems = items.slice(startIndex, endIndex)
  const totalHeight = items.length * itemHeight
  const offsetY = startIndex * itemHeight

  return (
    <div
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={(e) => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: totalHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, i) => (
            <div key={startIndex + i} style={{ height: itemHeight }}>
              {item.name}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}
// Recommended mature libraries: react-virtuoso, @tanstack/react-virtual, vue-virtual-scroller

// ========== 4. requestAnimationFrame for Animation ==========
// ✅ Synchronizes with the browser's refresh rate, no dropped frames
function animate() {
  element.style.transform = `translateX(${x}px)`
  x += speed
  if (x < target) {
    requestAnimationFrame(animate)
  }
}
requestAnimationFrame(animate)

// ========== 5. requestIdleCallback for Idle Tasks ==========
// Execute low-priority tasks when the browser is idle
function processLargeData(data) {
  let index = 0

  function processChunk(deadline) {
    while (index < data.length && deadline.timeRemaining() > 0) {
      processItem(data[index])
      index++
    }
    if (index < data.length) {
      requestIdleCallback(processChunk)
    }
  }
  requestIdleCallback(processChunk)
}

// ========== 6. Web Worker for CPU-Intensive Tasks ==========
// Main thread
const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' })
worker.postMessage({ type: 'sort', data: largeArray })
worker.onmessage = (e) => {
  console.log('Sorting complete:', e.data)
}

// worker.ts
self.onmessage = (e) => {
  if (e.data.type === 'sort') {
    const sorted = e.data.data.sort((a, b) => a - b)
    self.postMessage(sorted)
  }
}

6. What are the methods for React performance optimization?

// ========== 1. React.memo to avoid unnecessary re-renders ==========
const ExpensiveComponent = React.memo(({ data, onItemClick }) => {
  console.log('ExpensiveComponent rendered')
  return (
    <ul>
      {data.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  )
})

// ========== 2. useMemo to cache computed results ==========
function ProductList({ products, filterText }) {
  // Only recalculates when products or filterText change
  const filteredProducts = useMemo(() => {
    return products.filter(p =>
      p.name.toLowerCase().includes(filterText.toLowerCase())
    )
  }, [products, filterText])

  return <ul>{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}

// ========== 3. useCallback to cache function references ==========
function Parent() {
  const [count, setCount] = useState(0)

  // Without useCallback, a new function is created on every render,
  // causing child components wrapped in React.memo to also re-render
  const handleClick = useCallback((id) => {
    console.log('clicked:', id)
  }, [])  // Empty dependency array, function reference never changes

  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <ExpensiveComponent data={items} onItemClick={handleClick} />
    </>
  )
}

// ========== 4. React.lazy + Suspense for Code Splitting ==========
const HeavyChart = React.lazy(() => import('./HeavyChart'))

function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <HeavyChart />
    </Suspense>
  )
}

// ========== 5. useTransition / useDeferredValue (React 18+) ==========
function SearchPage() {
  const [query, setQuery] = useState('')
  const [isPending, startTransition] = useTransition()

  const handleChange = (e) => {
    // Update the input field immediately
    setQuery(e.target.value)

    // Defer updating search results (low priority)
    startTransition(() => {
      setSearchResults(filterData(e.target.value))
    })
  }

  return (
    <>
      <input value={query} onChange={handleChange} />
      {isPending ? <Spinner /> : <SearchResults />}
    </>
  )
}

// useDeferredValue: Defer updating a value
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query)
  // deferredQuery updates when the browser is idle, without blocking input

  const results = useMemo(() => filterData(deferredQuery), [deferredQuery])
  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
}

// ========== 6. State Colocation / Lifting State Down ==========
// ❌ State placed at the top level causes the entire tree to re-render
function App() {
  const [inputValue, setInputValue] = useState('')
  return (
    <div>
      <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
      <ExpensiveTree />  {/* Re-renders on every keystroke */}
    </div>
  )
}

// ✅ Colocate frequently changing state into a smaller component
function SearchInput() {
  const [inputValue, setInputValue] = useState('')
  return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
}

function App() {
  return (
    <div>
      <SearchInput />    {/* Only this small component re-renders */}
      <ExpensiveTree />  {/* Unaffected */}
    </div>
  )
}

💡 Bonus interview point: React 19 introduced the React Compiler, which automatically adds memoization to components. In the future, manually writing useMemo/useCallback may not be necessary. However, until then, manual optimization remains important for large list rendering and complex calculations.


7. What are the methods for Vue performance optimization?

<!-- ========== 1. v-once: Render only once ========== -->
<template>
  <div v-once>
    <!-- This block renders only once, subsequent updates are skipped -->
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
  </div>
</template>

<!-- ========== 2. v-memo: Conditional caching (Vue 3.2+) ========== -->
<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
    <!-- Only re-renders this node when item.selected changes -->
    <p>{{ item.name }}</p>
    <p>{{ item.selected ? '✅' : '❌' }}</p>
  </div>
</template>

<!-- ========== 3. Computed Property Caching ========== -->
<script setup>
import { computed, shallowRef, shallowReactive } from 'vue'

// computed has caching, only recalculates when dependencies change
const sortedList = computed(() => {
  return [...list.value].sort((a, b) => a.score - b.score)
})

// ========== 4. shallowRef / shallowReactive ==========
// For large objects where only the first level needs to be reactive
const hugeData = shallowRef({ list: [], meta: {} })
// Need to replace the entire reference when updating
hugeData.value = { ...hugeData.value, list: newList }

// ========== 5. Virtual List ==========
// Using vue-virtual-scroller
import { RecycleScroller } from 'vue-virtual-scroller'
</script>

<template>
  <RecycleScroller
    :items="items"
    :item-size="50"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="item">{{ item.name }}</div>
  </RecycleScroller>
</template>

<!-- ========== 6. Async Component Loading ========== -->
<script setup>
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent({
  loader: () => import('./HeavyChart.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200,          // Delay showing loading by 200ms
  timeout: 10000,      // Timeout duration
  errorComponent: ErrorDisplay
})

// ========== 7. KeepAlive to Cache Components ==========
</script>

<template>
  <KeepAlive :max="10" :include="['Home', 'Dashboard']">
    <router-view />
  </KeepAlive>
</template>

<!-- ========== 8. v-show vs v-if ========== -->
<!-- v-show: Use for frequent toggling (just display:none, DOM always exists) -->
<div v-show="isVisible">Frequently toggled content</div>
<!-- v-if: Use for infrequent toggling (DOM is not rendered when condition is false) -->
<div v-if="isLoaded">Content that won't toggle after initialization</div>

8. Bundle size optimization strategies?

// ========== 1. Tree Shaking ==========
// Ensure ES Module imports are used
import { debounce } from 'lodash-es'    // ✅ Tree-shakeable
// import _ from 'lodash'               // ❌ Imports the entire library

// ========== 2. On-Demand Import of UI Component Libraries ==========
// Use unplugin-vue-components / babel-plugin-import
// ❌ import ElementPlus from 'element-plus'     // Full import ~800KB
// ✅ import { ElButton, ElInput } from 'element-plus'  // On-demand import

// ========== 3. Code Splitting ==========
// Vite
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-lib': ['element-plus'],
          'chart': ['echarts']
        }
      }
    }
  }
})

// ========== 4. Compression ==========
// JS compression: esbuild (Vite default) / terser
// CSS compression: cssnano / lightningcss
// HTML compression: html-minifier
// Image compression: squoosh / sharp / imagemin
// Text compression: Gzip / Brotli

// ========== 5. Externalize Large Dependencies via CDN ==========
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      external: ['react', 'react-dom'],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
      }
    }
  }
})

// index.html loads from CDN
// <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>

// ========== 6. Analyze Bundle Size ==========
// Vite
import { visualizer } from 'rollup-plugin-visualizer'
// Webpack
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

// ========== 7. Dynamic Polyfills ==========
// Load polyfills on demand based on the browser, instead of a full import
<script src="https://polyfill.io/v3/polyfill.min.js?features=es2020"></script>

// ========== 8. Replace with Smaller Libraries ==========
// moment.js (~300KB) → dayjs (~2KB)
// lodash (~70KB) → lodash-es (Tree Shakeable)
// axios (~13KB) → ky (~3KB) or native fetch

9. Network optimization strategies?

// ========== 1. HTTP/2 Multiplexing ==========
// HTTP/1.1: Max 6 concurrent connections per domain
// HTTP/2: Single connection multiplexing, no concurrency limit
// Recommendation: Use HTTP/2 + reasonable code splitting (no need to bundle files)

// ========== 2. Resource Compression ==========
// Brotli compression is better than Gzip (usually 15-25% smaller)
// Nginx configuration
// gzip on;
// gzip_types text/css application/javascript application/json;
// brotli on;
// brotli_types text/css application/javascript application/json;

// ========== 3. CDN Acceleration ==========
// Deploy static resources to a CDN, leveraging edge nodes for nearby access
// Vite configuration
export default defineConfig({
  base: 'https://cdn.example.com/assets/'
})

// ========== 4. Request Optimization ==========
// API aggregation: BFF layer aggregates multiple APIs
// Request deduplication: Reuse the same request while it's pending
class RequestDedup {
  pendingMap = new Map()

  async request(key, fetcher) {
    if (this.pendingMap.has(key)) {
      return this.pendingMap.get(key)  // Reuse the pending request
    }
    const promise = fetcher().finally(() => this.pendingMap.delete(key))
    this.pendingMap.set(key, promise)
    return promise
  }
}

// ========== 5. Data Prefetching ==========
// Prefetch data before the user might access it
// React Query
const { data } = useQuery(['user', userId], fetchUser)
// Prefetch the next page
queryClient.prefetchQuery(['users', page + 1], () => fetchUsers(page + 1))

// ========== 6. WebSocket / SSE instead of Polling ==========
// ❌ Polling: Send a request every 3 seconds
setInterval(() => fetch('/api/notifications'), 3000)

// ✅ SSE: Server actively pushes data
const eventSource = new EventSource('/api/notifications/stream')
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data)
  updateNotifications(data)
}

// ========== 7. Use 103 Early Hints ==========
// Before the server finishes processing the HTML, it sends a 103 hint to tell the browser to preload resources
// 103 Early Hints
// Link: </style.css>; rel=preload; as=style
// Link: </main.js>; rel=preload; as=script

10. How to troubleshoot and avoid memory leaks?

// ========== Common Memory Leak Scenarios ==========

// 1. Event listeners not removed
// ❌
function setup() {
  window.addEventListener('resize', handleResize)
}
// ✅
function setup() {
  window.addEventListener('resize', handleResize)
  return () => window.removeEventListener('resize', handleResize)  // Cleanup
}

// In React
useEffect(() => {
  window.addEventListener('resize', handleResize)
  return () => window.removeEventListener('resize', handleResize)
}, [])

// In Vue
onMounted(() => window.addEventListener('resize', handleResize))
onUnmounted(() => window.removeEventListener('resize', handleResize))

// 2. Timers not cleared
// ❌
setInterval(() => fetchData(), 5000)
// ✅
const timer = setInterval(() => fetchData(), 5000)
onUnmounted(() => clearInterval(timer))

// 3. Closures holding references to large objects
// ❌
function createClosure() {
  const hugeArray = new Array(1000000).fill('data')
  return function() {
    console.log(hugeArray.length)  // Closure holds hugeArray, cannot be GC'd
  }
}
// ✅ Release when done
function createClosure() {
  let hugeArray = new Array(1000000).fill('data')
  const length = hugeArray.length
  hugeArray = null  // Release reference
  return function() {
    console.log(length)
  }
}

// 4. Residual DOM references
// ❌
const elements = []
function addElement() {
  const el = document.createElement('div')
  document.body.appendChild(el)
  elements.push(el)  // DOM reference retained in JS
}
function removeElement() {
  const el = elements.pop()
  el.remove()  // DOM removed, but reference in elements array remains
}

// 5. Map/Set not cleaned up
// ❌
const cache = new Map()
function cacheData(key, data) {
  cache.set(key, data)  // Cache grows indefinitely
}
// ✅ Use WeakMap or set a limit
const cache = new WeakMap()  // Automatically cleaned up when the key object is GC'd

// Or LRU Cache
class LRUCache {
  constructor(maxSize = 100) {
    this.maxSize = maxSize
    this.cache = new Map()
  }
  get(key) {
    if (!this.cache.has(key)) return undefined
    const value = this.cache.get(key)
    this.cache.delete(key)
    this.cache.set(key, value)  // Move to most recent
    return value
  }
  set(key, value) {
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)  // Delete the oldest
    }
    this.cache.set(key, value)
  }
}

// ========== Troubleshooting Tools ==========
// 1. Chrome DevTools → Memory panel
//    - Heap Snapshot: View memory snapshots, compare two snapshots to find leaks
//    - Allocation Timeline: Record memory allocation timeline
//    - Allocation Sampling: Sampling analysis

// 2. Performance Monitor
//    Chrome → More tools → Performance monitor
//    Observe if JS Heap Size is continuously growing

11. How to optimize white screen time?

// White screen time = Time from requesting the page to the first content render
// Core strategy: Reduce the Critical Rendering Path

// ========== 1. Reduce HTML Size ==========
// Compress HTML, remove comments and whitespace

// ========== 2. Inline Critical CSS ==========
// Inline the first-screen CSS into the <head> to prevent CSS from blocking rendering
<head>
  <style>
    /* First-screen critical CSS */
    .header { ... }
    .hero { ... }
  </style>
  <!-- Non-critical CSS loaded asynchronously -->
  <link rel="preload" href="/styles/full.css" as="style"
        onload="this.onload=null;this.rel='stylesheet'">
</head>

// ========== 3. Async JS Loading ==========
// defer does not block HTML parsing
<script defer src="/main.js"></script>

// ========== 4. Server-Side Rendering (SSR) ==========
// Returns complete HTML directly, no need to wait for JS to download and execute
// Next.js / Nuxt.js / Remix

// ========== 5. Prerendering ==========
// Generate static HTML at build time, suitable for pages with infrequently changing content
// vite-plugin-ssr / @prerenderer/webpack-plugin

// ========== 6. Loading Animation ==========
// Display a loading indicator before JS loads to improve user perception
<div id="app">
  <!-- This loading indicator displays before JS loads -->
  <div id="loading">
    <div class="spinner"></div>
    <p>Loading...</p>
  </div>
</div>

// ========== 7. Use CDN + HTTP/2 ==========
// Reduce network latency

// ========== 8. Reduce Redirects ==========
// Each redirect adds one RTT
// http → https → www → actual page (three redirects!)
// Should go directly to: https://www.example.com

12. Best practices for image optimization?

<!-- ========== 1. Choose the Right Image Format ========== -->
<!--
  AVIF: Best compression ratio, newer compatibility
  WebP: Excellent compression ratio, widely supported
  JPEG: Universal choice for photographic images
  PNG: Images requiring transparency
  SVG: Icons, logos, simple graphics
  JPEG XL: Future trend (limited browser support)
-->

<!-- ========== 2. Responsive Images ========== -->
<picture>
  <!-- Modern formats first -->
  <source srcset="photo.avif" type="image/avif">
  <source srcset="photo.webp" type="image/webp">
  <!-- Different screen sizes -->
  <source srcset="photo-mobile.jpg" media="(max-width: 768px)">
  <source srcset="photo-tablet.jpg" media="(max-width: 1200px)">
  <img src="photo.jpg" alt="Product image" loading="lazy" decoding="async">
</picture>

<!-- srcset + sizes for pixel density adaptation -->
<img
  srcset="image-1x.jpg 1x, image-2x.jpg 2x, image-3x.jpg 3x"
  src="image-2x.jpg"
  alt="retina adaptation"
>

<!-- ========== 3. Lazy Loading ========== -->
<!-- Native lazy loading -->
<img src="photo.jpg" loading="lazy" alt="">

<!-- First-screen images use eager loading + fetchpriority -->
<img src="hero.jpg" loading="eager" fetchpriority="high" alt="">

<!-- ========== 4. Declare Image Dimensions (to avoid CLS) ========== -->
<!-- Always declare width and height -->
<img src="photo.jpg" width="800" height="600" alt="">
<!-- Or use CSS aspect-ratio -->
<style>
  .img-container {
    aspect-ratio: 16 / 9;
    width: 100%;
  }
</style>
// ========== 5. Build-Time Image Compression ==========
// Vite plugin
import viteImagemin from 'vite-plugin-imagemin'

export default defineConfig({
  plugins: [
    viteImagemin({
      gifsicle: { optimizationLevel: 7 },
      mozjpeg: { quality: 75 },
      pngquant: { quality: [0.65, 0.9] },
      svgo: { plugins: [{ removeViewBox: false }] },
      webp: { quality: 75 }
    })
  ]
})

// ========== 6. Image CDN + Real-time Cropping ==========
// Use image CDN services (e.g., Cloudinary, Alibaba Cloud OSS, Tencent Cloud COS)
// Automatically crop and compress based on device
const imageUrl = `https://cdn.example.com/image.jpg?w=400&h=300&q=80&format=webp`

13. How to build a frontend monitoring system?

// ========== 1. Performance Monitoring ==========
// Use web-vitals to collect core metrics
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals'

function reportMetric(metric) {
  // Report to monitoring platform
  navigator.sendBeacon('/api/metrics', JSON.stringify({
    name: metric.name,
    value: metric.value,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
    url: location.href,
    timestamp: Date.now()
  }))
}

onLCP(reportMetric)
onINP(reportMetric)
onCLS(reportMetric)
onFCP(reportMetric)
onTTFB(reportMetric)

// ========== 2. Error Monitoring ==========
// JS runtime errors
window.addEventListener('error', (event) => {
  reportError({
    type: 'runtime',
    message: event.message,
    filename: event.filename,
    lineno: event.lineno,
    colno: event.colno,
    stack: event.error?.stack
  })
})

// Unhandled Promise rejections
window.addEventListener('unhandledrejection', (event) => {
  reportError({
    type: 'unhandledrejection',
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack
  })
})

// Resource loading errors
window.addEventListener('error', (event) => {
  if (event.target && (event.target.src || event.target.href)) {
    reportError({
      type: 'resource',
      url: event.target.src || event.target.href,
      tagName: event.target.tagName
    })
  }
}, true)  // Capture phase

// ========== 3. User Behavior Monitoring ==========
// Clicks, route changes, dwell time, etc.
document.addEventListener('click', (e) => {
  const target = e.target.closest('[data-track]')
  if (target) {
    reportEvent({
      type: 'click',
      element: target.dataset.track,
      text: target.textContent?.slice(0, 50)
    })
  }
})

// ========== 4. API Monitoring ==========
// Override fetch / XMLHttpRequest to monitor API performance
const originalFetch = window.fetch
window.fetch = async function(...args) {
  const start = performance.now()
  try {
    const response = await originalFetch.apply(this, args)
    const duration = performance.now() - start
    reportApi({
      url: args[0],
      status: response.status,
      duration,
      success: response.ok
    })
    return response
  } catch (error) {
    reportApi({ url: args[0], status: 0, error: error.message })
    throw error
  }
}

💡 Bonus interview point: In production environments, it is recommended to use mature monitoring platforms (Sentry, Tencent Frontend Monitoring TAM, Alibaba ARMS), which provide complete capabilities such as Source Map reverse parsing, error aggregation, and alerting.


14. Service Worker and PWA caching strategies?

// Service Worker can intercept network requests to implement offline caching

// ========== Register Service Worker ==========
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('SW registered successfully:', reg.scope))
      .catch(err => console.error('SW registration failed:', err))
  })
}

// sw.js
const CACHE_NAME = 'app-v1'
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/css/main.css',
  '/js/main.js'
]

// Install: Pre-cache core resources
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
  )
  self.skipWaiting()  // Activate immediately
})

// Activate: Clean up old caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
    )
  )
})

// ========== Caching Strategies ==========

// 1. Cache First: Suitable for static resources
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then(cached => cached || fetch(event.request))
  )
})

// 2. Network First: Suitable for API data
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          const clone = response.clone()
          caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone))
          return response
        })
        .catch(() => caches.match(event.request))
    )
  }
})

// 3. Stale While Revalidate: Suitable for infrequently changing resources
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.open(CACHE_NAME).then(cache =>
      cache.match(event.request).then(cached => {
        const fetched = fetch(event.request).then(response => {
          cache.put(event.request, response.clone())
          return response
        })
        return cached || fetched
      })
    )
  )
})

// It is recommended to use the Workbox library to simplify SW development
// import { precacheAndRoute, registerRoute } from 'workbox-precaching'
// import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'

15. What is the overall methodology for performance optimization?

Frontend Performance Optimization Panorama:

┌─────────────────────────────────────┐
│            Network Layer             │
│  ├── CDN Acceleration                │
│  ├── HTTP/2 / HTTP/3                 │
│  ├── Gzip / Brotli Compression       │
│  ├── DNS Prefetch / Preconnect       │
│  ├── Reasonable Caching Strategy     │
│  └── Reduce Request Count and Size   │
├─────────────────────────────────────┤
│            Resource Layer            │
│  ├── Code Splitting + On-Demand Load │
│  ├── Tree Shaking                    │
│  ├── Image Optimization              │
│  ├── Font Optimization               │
│  ├── Third-Party Library Slimming    │
│  └── Preload Critical Resources      │
├─────────────────────────────────────┤
│            Rendering Layer           │
│  ├── Inline Critical CSS             │
│  ├── Async JS Loading (defer/async)  │
│  ├── Reduce DOM Operations           │
│  ├── Avoid Forced Synchronous Layout │
│  ├── CSS Animations with transform/opacity │
│  ├── Virtual List                    │
│  └── SSR / SSG                       │
├─────────────────────────────────────┤
│            Runtime Layer             │
│  ├── Debounce/Throttle               │
│  ├── Web Worker                      │
│  ├── requestIdleCallback             │
│  ├── Memory Leak Prevention          │
│  └── Framework-Level Optimization    │
├─────────────────────────────────────┤
│            Experience Layer          │
│  ├── Skeleton Screens                │
│  ├── Progressive Loading             │
│  ├── Optimistic Updates              │
│  ├── Loading State Indicators        │
│  └── Offline Support (PWA)           │
├─────────────────────────────────────┤
│            Measurement Layer         │
│  ├── Core Web Vitals                 │
│  ├── Lighthouse                      │
│  ├── Performance API                 │
│  ├── Chrome DevTools                 │
│  └── Frontend Monitoring Platform    │
└─────────────────────────────────────┘

💡 Bonus interview point: Performance optimization is not a one-time task, but a continuous process. The correct approach: 1) Measure first (Lighthouse, Web Vitals) → 2) Analyze bottlenecks (Performance panel, BundleAnalyzer) → 3) Targeted optimization → 4) Verify results → 5) Establish monitoring to prevent regressions.