[{"data":1,"prerenderedAt":7381},["ShallowReactive",2],{"blog-post-core-web-vitals-nuxt-guide":3,"blog-posts":714,"content-query-mcqYLQfe4x":6834},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"readingTime":11,"tags":12,"featured":6,"body":15,"_type":708,"_id":709,"_source":710,"_file":711,"_stem":712,"_extension":713},"\u002Fblog\u002Fcore-web-vitals-nuxt-guide","blog",false,"","Core Web Vitals in Nuxt: Mastering LCP, FID, and CLS","Complete guide to measuring and optimizing Core Web Vitals in Nuxt applications. Learn how to achieve green scores on Google PageSpeed Insights with real strategies.","2024-06-28",11,[13,14],"Performance","Nuxt",{"type":16,"children":17,"toc":680},"root",[18,27,33,114,120,127,137,142,148,153,164,169,180,185,194,200,205,211,216,225,230,248,254,263,269,274,283,289,294,305,311,316,322,331,337,346,352,361,367,372,421,427,436,442,451,457,466,472,477,582,588,593,602,607,616,622,675],{"type":19,"tag":20,"props":21,"children":23},"element","h2",{"id":22},"understanding-core-web-vitals",[24],{"type":25,"value":26},"text","Understanding Core Web Vitals",{"type":19,"tag":28,"props":29,"children":30},"p",{},[31],{"type":25,"value":32},"Google's Core Web Vitals measure three aspects of user experience:",{"type":19,"tag":34,"props":35,"children":36},"ol",{},[37,63,91],{"type":19,"tag":38,"props":39,"children":40},"li",{},[41,47,49],{"type":19,"tag":42,"props":43,"children":44},"strong",{},[45],{"type":25,"value":46},"LCP (Largest Contentful Paint)",{"type":25,"value":48}," — How fast does content appear?",{"type":19,"tag":50,"props":51,"children":52},"ul",{},[53,58],{"type":19,"tag":38,"props":54,"children":55},{},[56],{"type":25,"value":57},"Target: \u003C 2.5s",{"type":19,"tag":38,"props":59,"children":60},{},[61],{"type":25,"value":62},"Measures: When largest element (image, heading, video) becomes visible",{"type":19,"tag":38,"props":64,"children":65},{},[66,71,73],{"type":19,"tag":42,"props":67,"children":68},{},[69],{"type":25,"value":70},"FID (First Input Delay)",{"type":25,"value":72}," — How responsive is the page?",{"type":19,"tag":50,"props":74,"children":75},{},[76,81,86],{"type":19,"tag":38,"props":77,"children":78},{},[79],{"type":25,"value":80},"Target: \u003C 100ms",{"type":19,"tag":38,"props":82,"children":83},{},[84],{"type":25,"value":85},"Measures: Delay from user input to browser processing",{"type":19,"tag":38,"props":87,"children":88},{},[89],{"type":25,"value":90},"Being replaced by INP in 2024",{"type":19,"tag":38,"props":92,"children":93},{},[94,99,101],{"type":19,"tag":42,"props":95,"children":96},{},[97],{"type":25,"value":98},"CLS (Cumulative Layout Shift)",{"type":25,"value":100}," — How stable is the layout?",{"type":19,"tag":50,"props":102,"children":103},{},[104,109],{"type":19,"tag":38,"props":105,"children":106},{},[107],{"type":25,"value":108},"Target: \u003C 0.1",{"type":19,"tag":38,"props":110,"children":111},{},[112],{"type":25,"value":113},"Measures: Unexpected visual shifts during page load",{"type":19,"tag":20,"props":115,"children":117},{"id":116},"measuring-web-vitals",[118],{"type":25,"value":119},"Measuring Web Vitals",{"type":19,"tag":121,"props":122,"children":124},"h3",{"id":123},"_1-google-pagespeed-insights",[125],{"type":25,"value":126},"1. Google PageSpeed Insights",{"type":19,"tag":128,"props":129,"children":131},"pre",{"code":130},"https:\u002F\u002Fpagespeed.web.dev\u002F?url=yoursite.com\n",[132],{"type":19,"tag":133,"props":134,"children":135},"code",{"__ignoreMap":7},[136],{"type":25,"value":130},{"type":19,"tag":28,"props":138,"children":139},{},[140],{"type":25,"value":141},"Shows real-world data from actual users. This is what matters most.",{"type":19,"tag":121,"props":143,"children":145},{"id":144},"_2-local-testing",[146],{"type":25,"value":147},"2. Local Testing",{"type":19,"tag":28,"props":149,"children":150},{},[151],{"type":25,"value":152},"Install web-vitals library:",{"type":19,"tag":128,"props":154,"children":159},{"code":155,"language":156,"meta":7,"className":157},"npm install web-vitals\n","bash",[158],"language-bash",[160],{"type":19,"tag":133,"props":161,"children":162},{"__ignoreMap":7},[163],{"type":25,"value":155},{"type":19,"tag":28,"props":165,"children":166},{},[167],{"type":25,"value":168},"Track metrics in Nuxt:",{"type":19,"tag":128,"props":170,"children":175},{"code":171,"language":172,"meta":7,"className":173},"\u002F\u002F plugins\u002Fweb-vitals.ts\nimport { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals'\n\nexport default defineNuxtPlugin(() => {\n  getCLS(console.log)\n  getFCP(console.log)\n  getFID(console.log)\n  getLCP(console.log)\n  getTTFB(console.log)\n})\n","typescript",[174],"language-typescript",[176],{"type":19,"tag":133,"props":177,"children":178},{"__ignoreMap":7},[179],{"type":25,"value":171},{"type":19,"tag":28,"props":181,"children":182},{},[183],{"type":25,"value":184},"Send to analytics:",{"type":19,"tag":128,"props":186,"children":189},{"code":187,"language":172,"meta":7,"className":188},"import { getCLS, getLCP, getFID } from 'web-vitals'\n\nconst vitals = {\n  cls: null,\n  lcp: null,\n  fid: null\n}\n\ngetCLS((metric) => {\n  vitals.cls = metric.value\n  sendToAnalytics('CLS', metric.value)\n})\n\ngetLCP((metric) => {\n  vitals.lcp = metric.value\n  sendToAnalytics('LCP', metric.value)\n})\n\ngetFID((metric) => {\n  vitals.fid = metric.value\n  sendToAnalytics('FID', metric.value)\n})\n",[174],[190],{"type":19,"tag":133,"props":191,"children":192},{"__ignoreMap":7},[193],{"type":25,"value":187},{"type":19,"tag":20,"props":195,"children":197},{"id":196},"optimizing-lcp",[198],{"type":25,"value":199},"Optimizing LCP",{"type":19,"tag":28,"props":201,"children":202},{},[203],{"type":25,"value":204},"LCP is usually the main blocker. Focus here first.",{"type":19,"tag":121,"props":206,"children":208},{"id":207},"strategy-1-optimize-largest-element",[209],{"type":25,"value":210},"Strategy 1: Optimize Largest Element",{"type":19,"tag":28,"props":212,"children":213},{},[214],{"type":25,"value":215},"Identify what's causing slow LCP:",{"type":19,"tag":128,"props":217,"children":220},{"code":218,"language":172,"meta":7,"className":219},"\u002F\u002F Measure element rendering\nconst observer = new PerformanceObserver((entryList) => {\n  for (const entry of entryList.getEntries()) {\n    if (entry.entryType === 'largest-contentful-paint') {\n      console.log('LCP Element:', entry.element)\n      console.log('LCP Size:', entry.size)\n      console.log('LCP Time:', entry.renderTime || entry.loadTime)\n    }\n  }\n})\n\nobserver.observe({ entryTypes: ['largest-contentful-paint'] })\n",[174],[221],{"type":19,"tag":133,"props":222,"children":223},{"__ignoreMap":7},[224],{"type":25,"value":218},{"type":19,"tag":28,"props":226,"children":227},{},[228],{"type":25,"value":229},"Usually the largest element is:",{"type":19,"tag":50,"props":231,"children":232},{},[233,238,243],{"type":19,"tag":38,"props":234,"children":235},{},[236],{"type":25,"value":237},"Hero image",{"type":19,"tag":38,"props":239,"children":240},{},[241],{"type":25,"value":242},"Main heading",{"type":19,"tag":38,"props":244,"children":245},{},[246],{"type":25,"value":247},"Large text block",{"type":19,"tag":121,"props":249,"children":251},{"id":250},"strategy-2-preload-critical-resources",[252],{"type":25,"value":253},"Strategy 2: Preload Critical Resources",{"type":19,"tag":128,"props":255,"children":258},{"code":256,"language":172,"meta":7,"className":257},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  app: {\n    head: {\n      link: [\n        \u002F\u002F Preload hero image\n        {\n          rel: 'preload',\n          as: 'image',\n          href: '\u002Fhero.webp',\n          imagesrcset: '\u002Fhero-mobile.webp 600w, \u002Fhero-desktop.webp 1200w',\n          imagesizes: '(max-width: 600px) 600px, 1200px'\n        },\n        \u002F\u002F Preload critical fonts\n        {\n          rel: 'preload',\n          as: 'font',\n          href: '\u002Ffonts\u002Finter.woff2',\n          type: 'font\u002Fwoff2',\n          crossorigin: ''\n        },\n        \u002F\u002F DNS prefetch for external APIs\n        {\n          rel: 'dns-prefetch',\n          href: 'https:\u002F\u002Fapi.example.com'\n        },\n        \u002F\u002F Preconnect to critical domains\n        {\n          rel: 'preconnect',\n          href: 'https:\u002F\u002Fapi.example.com'\n        }\n      ]\n    }\n  }\n})\n",[174],[259],{"type":19,"tag":133,"props":260,"children":261},{"__ignoreMap":7},[262],{"type":25,"value":256},{"type":19,"tag":121,"props":264,"children":266},{"id":265},"strategy-3-server-side-rendering",[267],{"type":25,"value":268},"Strategy 3: Server-Side Rendering",{"type":19,"tag":28,"props":270,"children":271},{},[272],{"type":25,"value":273},"SSR ensures content is in HTML, not dependent on JavaScript:",{"type":19,"tag":128,"props":275,"children":278},{"code":276,"language":172,"meta":7,"className":277},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    \u002F\u002F Cache static pages at build time\n    '\u002F**': { cache: { maxAge: 60 * 10 } },\n    '\u002Fapi\u002F**': { noCache: true },\n    '\u002Fadmin\u002F**': { ssr: false } \u002F\u002F Client-side only for admin\n  }\n})\n",[174],[279],{"type":19,"tag":133,"props":280,"children":281},{"__ignoreMap":7},[282],{"type":25,"value":276},{"type":19,"tag":121,"props":284,"children":286},{"id":285},"strategy-4-image-optimization",[287],{"type":25,"value":288},"Strategy 4: Image Optimization",{"type":19,"tag":28,"props":290,"children":291},{},[292],{"type":25,"value":293},"Hero images often block LCP. Optimize aggressively:",{"type":19,"tag":128,"props":295,"children":300},{"code":296,"language":297,"meta":7,"className":298},"\u003Cscript setup>\nimport { useImage } from '#app'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv class=\"hero\">\n    \u003C!-- Use NuxtImg for automatic optimization -->\n    \u003CNuxtImg\n      src=\"\u002Fhero.jpg\"\n      alt=\"Hero\"\n      sizes=\"xs:100vw md:800px\"\n      quality=\"80\"\n      format=\"webp,jpg\"\n      loading=\"eager\"\n      fetchpriority=\"high\"\n    \u002F>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.hero {\n  \u002F* Prevent layout shift while image loads -->\n  aspect-ratio: 16 \u002F 9;\n}\n\u003C\u002Fstyle>\n","vue",[299],"language-vue",[301],{"type":19,"tag":133,"props":302,"children":303},{"__ignoreMap":7},[304],{"type":25,"value":296},{"type":19,"tag":20,"props":306,"children":308},{"id":307},"optimizing-fid-and-inp",[309],{"type":25,"value":310},"Optimizing FID (and INP)",{"type":19,"tag":28,"props":312,"children":313},{},[314],{"type":25,"value":315},"FID measures JavaScript execution blocking input. Reduce with:",{"type":19,"tag":121,"props":317,"children":319},{"id":318},"_1-code-splitting",[320],{"type":25,"value":321},"1. Code Splitting",{"type":19,"tag":128,"props":323,"children":326},{"code":324,"language":172,"meta":7,"className":325},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks: {\n          'vendor': ['lodash-es', 'date-fns'],\n          'charts': ['chart.js']\n        }\n      }\n    }\n  }\n})\n",[174],[327],{"type":19,"tag":133,"props":328,"children":329},{"__ignoreMap":7},[330],{"type":25,"value":324},{"type":19,"tag":121,"props":332,"children":334},{"id":333},"_2-web-workers-for-heavy-tasks",[335],{"type":25,"value":336},"2. Web Workers for Heavy Tasks",{"type":19,"tag":128,"props":338,"children":341},{"code":339,"language":172,"meta":7,"className":340},"\u002F\u002F composables\u002FuseExpensiveCalculation.ts\nexport const useExpensiveCalculation = () => {\n  const result = ref(null)\n  const processing = ref(false)\n\n  const calculate = async (data: any) => {\n    processing.value = true\n    \n    \u002F\u002F Offload to worker\n    const worker = new Worker(new URL('@\u002Fworkers\u002Fcalculation.ts', import.meta.url), {\n      type: 'module'\n    })\n    \n    worker.postMessage(data)\n    \n    await new Promise\u003Cvoid>((resolve) => {\n      worker.onmessage = (e) => {\n        result.value = e.data\n        processing.value = false\n        worker.terminate()\n        resolve()\n      }\n    })\n  }\n\n  return { result, processing, calculate }\n}\n",[174],[342],{"type":19,"tag":133,"props":343,"children":344},{"__ignoreMap":7},[345],{"type":25,"value":339},{"type":19,"tag":121,"props":347,"children":349},{"id":348},"_3-defer-non-critical-javascript",[350],{"type":25,"value":351},"3. Defer Non-Critical JavaScript",{"type":19,"tag":128,"props":353,"children":356},{"code":354,"language":297,"meta":7,"className":355},"\u003Cscript setup>\n\u002F\u002F Track analytics only after page is interactive\nonMounted(() => {\n  if (window.requestIdleCallback) {\n    requestIdleCallback(() => {\n      \u002F\u002F Load analytics after browser is idle\n      window.gtag?.('event', 'page_view')\n    })\n  } else {\n    setTimeout(() => {\n      window.gtag?.('event', 'page_view')\n    }, 2000)\n  }\n})\n\u003C\u002Fscript>\n",[299],[357],{"type":19,"tag":133,"props":358,"children":359},{"__ignoreMap":7},[360],{"type":25,"value":354},{"type":19,"tag":20,"props":362,"children":364},{"id":363},"optimizing-cls",[365],{"type":25,"value":366},"Optimizing CLS",{"type":19,"tag":28,"props":368,"children":369},{},[370],{"type":25,"value":371},"CLS is layout instability. Causes:",{"type":19,"tag":34,"props":373,"children":374},{},[375,385,395,411],{"type":19,"tag":38,"props":376,"children":377},{},[378,383],{"type":19,"tag":42,"props":379,"children":380},{},[381],{"type":25,"value":382},"Images without dimensions",{"type":25,"value":384}," — add width\u002Fheight",{"type":19,"tag":38,"props":386,"children":387},{},[388,393],{"type":19,"tag":42,"props":389,"children":390},{},[391],{"type":25,"value":392},"Ads loading late",{"type":25,"value":394}," — reserve space",{"type":19,"tag":38,"props":396,"children":397},{},[398,403,405],{"type":19,"tag":42,"props":399,"children":400},{},[401],{"type":25,"value":402},"Fonts loading",{"type":25,"value":404}," — use ",{"type":19,"tag":133,"props":406,"children":408},{"className":407},[],[409],{"type":25,"value":410},"font-display: swap",{"type":19,"tag":38,"props":412,"children":413},{},[414,419],{"type":19,"tag":42,"props":415,"children":416},{},[417],{"type":25,"value":418},"Dynamic content",{"type":25,"value":420}," — skeleton screens",{"type":19,"tag":121,"props":422,"children":424},{"id":423},"fix-1-image-dimensions",[425],{"type":25,"value":426},"Fix 1: Image Dimensions",{"type":19,"tag":128,"props":428,"children":431},{"code":429,"language":297,"meta":7,"className":430},"\u003C!-- ❌ Causes CLS -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" \u002F>\n\n\u003C!-- ✅ No CLS -->\n\u003Cimg src=\"photo.jpg\" alt=\"Photo\" width=\"800\" height=\"600\" \u002F>\n\n\u003C!-- Or use aspect-ratio -->\n\u003Cdiv class=\"image-container\">\n  \u003Cimg src=\"photo.jpg\" alt=\"Photo\" \u002F>\n\u003C\u002Fdiv>\n\n\u003Cstyle>\n.image-container {\n  aspect-ratio: 800 \u002F 600;\n}\n\u003C\u002Fstyle>\n",[299],[432],{"type":19,"tag":133,"props":433,"children":434},{"__ignoreMap":7},[435],{"type":25,"value":429},{"type":19,"tag":121,"props":437,"children":439},{"id":438},"fix-2-font-loading",[440],{"type":25,"value":441},"Fix 2: Font Loading",{"type":19,"tag":128,"props":443,"children":446},{"code":444,"language":172,"meta":7,"className":445},"\u002F\u002F Use font-display: swap to show text immediately\nexport default defineNuxtConfig({\n  app: {\n    head: {\n      link: [\n        {\n          rel: 'stylesheet',\n          href: 'https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap'\n        }\n      ]\n    }\n  }\n})\n",[174],[447],{"type":19,"tag":133,"props":448,"children":449},{"__ignoreMap":7},[450],{"type":25,"value":444},{"type":19,"tag":121,"props":452,"children":454},{"id":453},"fix-3-skeleton-screens",[455],{"type":25,"value":456},"Fix 3: Skeleton Screens",{"type":19,"tag":128,"props":458,"children":461},{"code":459,"language":297,"meta":7,"className":460},"\u003Cscript setup>\nconst post = ref(null)\nconst loading = ref(true)\n\nonMounted(async () => {\n  post.value = await fetchPost()\n  loading.value = false\n})\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Carticle v-if=\"loading\" class=\"post-skeleton\">\n    \u003Cdiv class=\"skeleton-title\" \u002F>\n    \u003Cdiv class=\"skeleton-text\" \u002F>\n    \u003Cdiv class=\"skeleton-text\" \u002F>\n  \u003C\u002Farticle>\n  \n  \u003Carticle v-else>\n    \u003Ch1>{{ post.title }}\u003C\u002Fh1>\n    \u003Cp>{{ post.content }}\u003C\u002Fp>\n  \u003C\u002Farticle>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.skeleton-title {\n  width: 80%;\n  height: 2rem;\n  background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);\n  background-size: 200% 100%;\n  animation: loading 1.5s infinite;\n  border-radius: 0.25rem;\n  margin-bottom: 1rem;\n}\n\n@keyframes loading {\n  0% { background-position: 200% 0; }\n  100% { background-position: -200% 0; }\n}\n\u003C\u002Fstyle>\n",[299],[462],{"type":19,"tag":133,"props":463,"children":464},{"__ignoreMap":7},[465],{"type":25,"value":459},{"type":19,"tag":20,"props":467,"children":469},{"id":468},"real-world-results",[470],{"type":25,"value":471},"Real-World Results",{"type":19,"tag":28,"props":473,"children":474},{},[475],{"type":25,"value":476},"After implementing these optimizations:",{"type":19,"tag":478,"props":479,"children":480},"table",{},[481,505],{"type":19,"tag":482,"props":483,"children":484},"thead",{},[485],{"type":19,"tag":486,"props":487,"children":488},"tr",{},[489,495,500],{"type":19,"tag":490,"props":491,"children":492},"th",{},[493],{"type":25,"value":494},"Metric",{"type":19,"tag":490,"props":496,"children":497},{},[498],{"type":25,"value":499},"Before",{"type":19,"tag":490,"props":501,"children":502},{},[503],{"type":25,"value":504},"After",{"type":19,"tag":506,"props":507,"children":508},"tbody",{},[509,528,546,564],{"type":19,"tag":486,"props":510,"children":511},{},[512,518,523],{"type":19,"tag":513,"props":514,"children":515},"td",{},[516],{"type":25,"value":517},"LCP",{"type":19,"tag":513,"props":519,"children":520},{},[521],{"type":25,"value":522},"3.8s",{"type":19,"tag":513,"props":524,"children":525},{},[526],{"type":25,"value":527},"1.6s",{"type":19,"tag":486,"props":529,"children":530},{},[531,536,541],{"type":19,"tag":513,"props":532,"children":533},{},[534],{"type":25,"value":535},"FID",{"type":19,"tag":513,"props":537,"children":538},{},[539],{"type":25,"value":540},"180ms",{"type":19,"tag":513,"props":542,"children":543},{},[544],{"type":25,"value":545},"45ms",{"type":19,"tag":486,"props":547,"children":548},{},[549,554,559],{"type":19,"tag":513,"props":550,"children":551},{},[552],{"type":25,"value":553},"CLS",{"type":19,"tag":513,"props":555,"children":556},{},[557],{"type":25,"value":558},"0.22",{"type":19,"tag":513,"props":560,"children":561},{},[562],{"type":25,"value":563},"0.04",{"type":19,"tag":486,"props":565,"children":566},{},[567,572,577],{"type":19,"tag":513,"props":568,"children":569},{},[570],{"type":25,"value":571},"PageSpeed Score",{"type":19,"tag":513,"props":573,"children":574},{},[575],{"type":25,"value":576},"42",{"type":19,"tag":513,"props":578,"children":579},{},[580],{"type":25,"value":581},"92",{"type":19,"tag":20,"props":583,"children":585},{"id":584},"continuous-monitoring",[586],{"type":25,"value":587},"Continuous Monitoring",{"type":19,"tag":28,"props":589,"children":590},{},[591],{"type":25,"value":592},"Set up alerts for regressions:",{"type":19,"tag":128,"props":594,"children":597},{"code":595,"language":172,"meta":7,"className":596},"\u002F\u002F Lighthouse CI\nnpm install -D @lhci\u002Fcli\n\n# lhciconfig.json\n{\n  \"ci\": {\n    \"collect\": {\n      \"url\": [\"https:\u002F\u002Fyoursite.com\"],\n      \"numberOfRuns\": 3\n    },\n    \"assert\": {\n      \"preset\": \"lighthouse:recommended\",\n      \"assertions\": {\n        \"largest-contentful-paint\": [\"error\", { \"maxNumericValue\": 2500 }],\n        \"first-input-delay\": [\"error\", { \"maxNumericValue\": 100 }],\n        \"cumulative-layout-shift\": [\"error\", { \"maxNumericValue\": 0.1 }]\n      }\n    }\n  }\n}\n",[174],[598],{"type":19,"tag":133,"props":599,"children":600},{"__ignoreMap":7},[601],{"type":25,"value":595},{"type":19,"tag":28,"props":603,"children":604},{},[605],{"type":25,"value":606},"Run in CI:",{"type":19,"tag":128,"props":608,"children":611},{"code":609,"language":156,"meta":7,"className":610},"# .github\u002Fworkflows\u002Flighthouse.yml\n- name: Run Lighthouse CI\n  run: lhci autorun\n",[158],[612],{"type":19,"tag":133,"props":613,"children":614},{"__ignoreMap":7},[615],{"type":25,"value":609},{"type":19,"tag":20,"props":617,"children":619},{"id":618},"key-takeaways",[620],{"type":25,"value":621},"Key Takeaways",{"type":19,"tag":34,"props":623,"children":624},{},[625,635,645,655,665],{"type":19,"tag":38,"props":626,"children":627},{},[628,633],{"type":19,"tag":42,"props":629,"children":630},{},[631],{"type":25,"value":632},"Measure real user data",{"type":25,"value":634}," — lab data differs from production",{"type":19,"tag":38,"props":636,"children":637},{},[638,643],{"type":19,"tag":42,"props":639,"children":640},{},[641],{"type":25,"value":642},"LCP is usually the bottleneck",{"type":25,"value":644}," — focus optimization efforts there",{"type":19,"tag":38,"props":646,"children":647},{},[648,653],{"type":19,"tag":42,"props":649,"children":650},{},[651],{"type":25,"value":652},"Images matter most",{"type":25,"value":654}," — proper optimization yields biggest gains",{"type":19,"tag":38,"props":656,"children":657},{},[658,663],{"type":19,"tag":42,"props":659,"children":660},{},[661],{"type":25,"value":662},"Preload critical resources",{"type":25,"value":664}," — hero images and fonts",{"type":19,"tag":38,"props":666,"children":667},{},[668,673],{"type":19,"tag":42,"props":669,"children":670},{},[671],{"type":25,"value":672},"Monitor continuously",{"type":25,"value":674}," — regressions happen. Catch them early",{"type":19,"tag":28,"props":676,"children":677},{},[678],{"type":25,"value":679},"Core Web Vitals are no longer just SEO metrics—they're user experience fundamentals. Invest in getting them right.",{"title":7,"searchDepth":681,"depth":681,"links":682},2,[683,684,689,695,700,705,706,707],{"id":22,"depth":681,"text":26},{"id":116,"depth":681,"text":119,"children":685},[686,688],{"id":123,"depth":687,"text":126},3,{"id":144,"depth":687,"text":147},{"id":196,"depth":681,"text":199,"children":690},[691,692,693,694],{"id":207,"depth":687,"text":210},{"id":250,"depth":687,"text":253},{"id":265,"depth":687,"text":268},{"id":285,"depth":687,"text":288},{"id":307,"depth":681,"text":310,"children":696},[697,698,699],{"id":318,"depth":687,"text":321},{"id":333,"depth":687,"text":336},{"id":348,"depth":687,"text":351},{"id":363,"depth":681,"text":366,"children":701},[702,703,704],{"id":423,"depth":687,"text":426},{"id":438,"depth":687,"text":441},{"id":453,"depth":687,"text":456},{"id":468,"depth":681,"text":471},{"id":584,"depth":681,"text":587},{"id":618,"depth":681,"text":621},"markdown","content:blog:core-web-vitals-nuxt-guide.md","content","blog\u002Fcore-web-vitals-nuxt-guide.md","blog\u002Fcore-web-vitals-nuxt-guide","md",[715,1259,1822,2367,3172,3719,4150,5009,5472,6206],{"_path":716,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":717,"description":718,"date":719,"updated":720,"readingTime":721,"tags":722,"featured":725,"body":726,"_type":708,"_id":1256,"_source":710,"_file":1257,"_stem":1258,"_extension":713},"\u002Fblog\u002Fvue-2-to-vue-3-migration-at-scale","Vue 2 to Vue 3 Migration at Scale: Lessons from a Production Migration","A guide to migrating large Vue 2 codebases to Vue 3 without downtime. Learn the strategies, tooling, and patterns that worked across dozens of components and the full test suite.","2024-09-15","2026-07-05",12,[723,724,13],"Vue","Architecture",true,{"type":16,"children":727,"toc":1244},[728,734,739,744,794,799,805,811,830,839,844,862,868,873,906,913,922,927,976,982,995,1001,1012,1017,1023,1032,1037,1043,1052,1057,1063,1068,1077,1082,1087,1093,1098,1106,1131,1137,1190,1196,1239],{"type":19,"tag":20,"props":729,"children":731},{"id":730},"the-challenge-why-we-needed-to-migrate",[732],{"type":25,"value":733},"The Challenge: Why We Needed to Migrate",{"type":19,"tag":28,"props":735,"children":736},{},[737],{"type":25,"value":738},"We faced a critical decision when Vue 3 became stable in 2021. Our Vue 2 codebase had grown large across multiple teams.",{"type":19,"tag":28,"props":740,"children":741},{},[742],{"type":25,"value":743},"The pain points were real:",{"type":19,"tag":50,"props":745,"children":746},{},[747,757,767,776],{"type":19,"tag":38,"props":748,"children":749},{},[750,755],{"type":19,"tag":42,"props":751,"children":752},{},[753],{"type":25,"value":754},"Type Safety",{"type":25,"value":756},": Vue 2 + TypeScript was never fully integrated. The IDE caught some issues; the rest surfaced in QA",{"type":19,"tag":38,"props":758,"children":759},{},[760,765],{"type":19,"tag":42,"props":761,"children":762},{},[763],{"type":25,"value":764},"Composition Reuse",{"type":25,"value":766},": Mixins led to naming collisions and unclear data flow. Multiple teams independently \"solved\" the same problems",{"type":19,"tag":38,"props":768,"children":769},{},[770,774],{"type":19,"tag":42,"props":771,"children":772},{},[773],{"type":25,"value":13},{"type":25,"value":775},": Bundle size kept growing. Tree-shaking didn't work well with Vue 2's runtime structure",{"type":19,"tag":38,"props":777,"children":778},{},[779,784,786,792],{"type":19,"tag":42,"props":780,"children":781},{},[782],{"type":25,"value":783},"Developer Experience",{"type":25,"value":785},": New team members struggled with implicit ",{"type":19,"tag":133,"props":787,"children":789},{"className":788},[],[790],{"type":25,"value":791},"this",{"type":25,"value":793}," context and composition patterns",{"type":19,"tag":28,"props":795,"children":796},{},[797],{"type":25,"value":798},"The business case was compelling: fewer bugs in production, faster onboarding, a smaller bundle.",{"type":19,"tag":20,"props":800,"children":802},{"id":801},"our-migration-strategy",[803],{"type":25,"value":804},"Our Migration Strategy",{"type":19,"tag":121,"props":806,"children":808},{"id":807},"phase-1-set-up-build-tooling-1-week",[809],{"type":25,"value":810},"Phase 1: Set Up Build Tooling (1 week)",{"type":19,"tag":28,"props":812,"children":813},{},[814,816,821,823,828],{"type":25,"value":815},"We chose ",{"type":19,"tag":42,"props":817,"children":818},{},[819],{"type":25,"value":820},"Vite",{"type":25,"value":822}," + ",{"type":19,"tag":42,"props":824,"children":825},{},[826],{"type":25,"value":827},"@vitejs\u002Fplugin-vue",{"type":25,"value":829}," instead of webpack. This decision paid dividends:",{"type":19,"tag":128,"props":831,"children":834},{"className":832,"code":833,"language":172,"meta":7},[174],"\u002F\u002F vite.config.ts\nexport default {\n  plugins: [vue(), visualizer()],\n  build: {\n    rollupOptions: {\n      output: { manualChunks: customChunks }\n    }\n  }\n}\n",[835],{"type":19,"tag":133,"props":836,"children":837},{"__ignoreMap":7},[838],{"type":25,"value":833},{"type":19,"tag":28,"props":840,"children":841},{},[842],{"type":25,"value":843},"Bundle analysis immediately showed why the tooling move pays for itself:",{"type":19,"tag":50,"props":845,"children":846},{},[847,852,857],{"type":19,"tag":38,"props":848,"children":849},{},[850],{"type":25,"value":851},"Vite's default code splitting cut the main bundle substantially before we touched a component",{"type":19,"tag":38,"props":853,"children":854},{},[855],{"type":25,"value":856},"Build times dropped from tens of seconds under webpack to single digits under Vite",{"type":19,"tag":38,"props":858,"children":859},{},[860],{"type":25,"value":861},"Hot Module Replacement went from a coffee-sip wait to effectively instant",{"type":19,"tag":121,"props":863,"children":865},{"id":864},"phase-2-incremental-component-migration-6-weeks",[866],{"type":25,"value":867},"Phase 2: Incremental Component Migration (6 weeks)",{"type":19,"tag":28,"props":869,"children":870},{},[871],{"type":25,"value":872},"We didn't rewrite everything. Instead, we:",{"type":19,"tag":34,"props":874,"children":875},{},[876,886,896],{"type":19,"tag":38,"props":877,"children":878},{},[879,884],{"type":19,"tag":42,"props":880,"children":881},{},[882],{"type":25,"value":883},"Identified high-ROI components",{"type":25,"value":885}," — components used across multiple products, or frequently modified",{"type":19,"tag":38,"props":887,"children":888},{},[889,894],{"type":19,"tag":42,"props":890,"children":891},{},[892],{"type":25,"value":893},"Created a co-existence layer",{"type":25,"value":895}," — Vue 2 and Vue 3 components lived side-by-side",{"type":19,"tag":38,"props":897,"children":898},{},[899,904],{"type":19,"tag":42,"props":900,"children":901},{},[902],{"type":25,"value":903},"Migrated by priority",{"type":25,"value":905}," — a steady per-engineer cadence, not a big-bang rewrite",{"type":19,"tag":907,"props":908,"children":910},"h4",{"id":909},"key-pattern-the-composition-api-advantage",[911],{"type":25,"value":912},"Key Pattern: The Composition API Advantage",{"type":19,"tag":128,"props":914,"children":917},{"className":915,"code":916,"language":172,"meta":7},[174],"\u002F\u002F Old Vue 2 (mixin-based)\nexport const userMixin = {\n  data() {\n    return { user: null, loading: false }\n  },\n  methods: {\n    async fetchUser(id) {\n      this.loading = true\n      this.user = await api.getUser(id)\n      this.loading = false\n    }\n  },\n  computed: {\n    isAuthor() {\n      return this.user?.id === this.currentUserId\n    }\n  }\n}\n\n\u002F\u002F New Vue 3 (Composition API)\nexport const useUser = (userId: Ref\u003Cstring>) => {\n  const user = ref\u003CUser | null>(null)\n  const loading = ref(false)\n  \n  const fetchUser = async (id: string) => {\n    loading.value = true\n    user.value = await api.getUser(id)\n    loading.value = false\n  }\n  \n  const isAuthor = computed(() => user.value?.id === userId.value)\n  \n  watchEffect(() => {\n    if (userId.value) fetchUser(userId.value)\n  })\n  \n  return { user, loading, isAuthor, fetchUser }\n}\n",[918],{"type":19,"tag":133,"props":919,"children":920},{"__ignoreMap":7},[921],{"type":25,"value":916},{"type":19,"tag":28,"props":923,"children":924},{},[925],{"type":25,"value":926},"The Composition API version:",{"type":19,"tag":50,"props":928,"children":929},{},[930,940,950,960],{"type":19,"tag":38,"props":931,"children":932},{},[933,938],{"type":19,"tag":42,"props":934,"children":935},{},[936],{"type":25,"value":937},"Eliminates naming collisions",{"type":25,"value":939}," — composables are just functions, no implicit merging",{"type":19,"tag":38,"props":941,"children":942},{},[943,948],{"type":19,"tag":42,"props":944,"children":945},{},[946],{"type":25,"value":947},"Makes data flow explicit",{"type":25,"value":949}," — you see exactly which data is used",{"type":19,"tag":38,"props":951,"children":952},{},[953,958],{"type":19,"tag":42,"props":954,"children":955},{},[956],{"type":25,"value":957},"Enables better tree-shaking",{"type":25,"value":959}," — unused composables are easier to identify",{"type":19,"tag":38,"props":961,"children":962},{},[963,968,970],{"type":19,"tag":42,"props":964,"children":965},{},[966],{"type":25,"value":967},"Improves TypeScript",{"type":25,"value":969}," — full type inference without ",{"type":19,"tag":133,"props":971,"children":973},{"className":972},[],[974],{"type":25,"value":975},"as any",{"type":19,"tag":121,"props":977,"children":979},{"id":978},"phase-3-handle-the-breaking-changes",[980],{"type":25,"value":981},"Phase 3: Handle the Breaking Changes",{"type":19,"tag":28,"props":983,"children":984},{},[985,987,993],{"type":25,"value":986},"Vue 3's breaking changes were documented, but the ",{"type":19,"tag":988,"props":989,"children":990},"em",{},[991],{"type":25,"value":992},"scale",{"type":25,"value":994}," matters. Here's what actually cost us the most time:",{"type":19,"tag":907,"props":996,"children":998},{"id":997},"_1-event-handling-3-days-spent",[999],{"type":25,"value":1000},"1. Event Handling (3 days spent)",{"type":19,"tag":128,"props":1002,"children":1007},{"className":1003,"code":1005,"language":1006,"meta":7},[1004],"language-javascript","\u002F\u002F Vue 2 - event modifiers worked universally\n\u003Cinput @keyup.enter=\"submit\" \u002F>\n\u003Cdiv @click.self=\"close\" \u002F>\n\n\u002F\u002F Vue 3 - custom components don't auto-inherit modifiers\n\u003CCustomInput @keyup.enter=\"submit\" \u002F> \u002F\u002F ❌ Won't work\n\n\u002F\u002F Solution: Explicitly bind in component\n\u003Cinput @keyup.enter=\"$emit('keyup-enter')\" \u002F>\n","javascript",[1008],{"type":19,"tag":133,"props":1009,"children":1010},{"__ignoreMap":7},[1011],{"type":25,"value":1005},{"type":19,"tag":28,"props":1013,"children":1014},{},[1015],{"type":25,"value":1016},"We had instances of this all over the codebase. Automated search + manual review took longer than the code changes themselves.",{"type":19,"tag":907,"props":1018,"children":1020},{"id":1019},"_2-async-components",[1021],{"type":25,"value":1022},"2. Async Components",{"type":19,"tag":128,"props":1024,"children":1027},{"className":1025,"code":1026,"language":1006,"meta":7},[1004],"\u002F\u002F Vue 2\nconst AsyncComponent = () => import('.\u002FAsyncComponent.vue')\n\n\u002F\u002F Vue 3 - requires `defineAsyncComponent`\nimport { defineAsyncComponent } from 'vue'\nconst AsyncComponent = defineAsyncComponent(\n  () => import('.\u002FAsyncComponent.vue')\n)\n",[1028],{"type":19,"tag":133,"props":1029,"children":1030},{"__ignoreMap":7},[1031],{"type":25,"value":1026},{"type":19,"tag":28,"props":1033,"children":1034},{},[1035],{"type":25,"value":1036},"Dozens more locations to update. Pro tip: codemods exist for this, but they're not perfect. Invest time in understanding edge cases.",{"type":19,"tag":907,"props":1038,"children":1040},{"id":1039},"_3-filters-computed-properties",[1041],{"type":25,"value":1042},"3. Filters → Computed Properties",{"type":19,"tag":128,"props":1044,"children":1047},{"className":1045,"code":1046,"language":1006,"meta":7},[1004],"\u002F\u002F Vue 2\n{{ message | capitalize }}\n\u002F\u002F Defined as: Vue.filter('capitalize', ...)\n\n\u002F\u002F Vue 3 - must be explicit\n{{ capitalizedMessage }}\n\u002F\u002F const capitalizedMessage = computed(() => capitalize(message.value))\n",[1048],{"type":19,"tag":133,"props":1049,"children":1050},{"__ignoreMap":7},[1051],{"type":25,"value":1046},{"type":19,"tag":28,"props":1053,"children":1054},{},[1055],{"type":25,"value":1056},"Dozens of filter usages. We created a utility module to centralize custom filters as functions.",{"type":19,"tag":121,"props":1058,"children":1060},{"id":1059},"phase-4-test-migration-parallel-4-weeks",[1061],{"type":25,"value":1062},"Phase 4: Test Migration (Parallel, 4 weeks)",{"type":19,"tag":28,"props":1064,"children":1065},{},[1066],{"type":25,"value":1067},"Our Jest test suite had to be updated to work with Vue 3. The good news: tests were actually easier to write.",{"type":19,"tag":128,"props":1069,"children":1072},{"className":1070,"code":1071,"language":172,"meta":7},[174],"\u002F\u002F Vue 2 test with vue-test-utils\nconst wrapper = mount(MyComponent, {\n  mocks: { $t: (key) => key },\n  propsData: { user: testUser }\n})\nexpect(wrapper.vm.formattedName).toBe('John Doe')\n\n\u002F\u002F Vue 3 test - cleaner syntax\nconst { vm } = mount(MyComponent, {\n  props: { user: testUser },\n  global: { mocks: { $t: (key) => key } }\n})\nexpect(vm.formattedName).toBe('John Doe')\n",[1073],{"type":19,"tag":133,"props":1074,"children":1075},{"__ignoreMap":7},[1076],{"type":25,"value":1071},{"type":19,"tag":28,"props":1078,"children":1079},{},[1080],{"type":25,"value":1081},"We migrated the full test suite.",{"type":19,"tag":28,"props":1083,"children":1084},{},[1085],{"type":25,"value":1086},"It took weeks, not days — but many tests revealed brittleness in the original code. This was actually valuable.",{"type":19,"tag":20,"props":1088,"children":1090},{"id":1089},"the-results",[1091],{"type":25,"value":1092},"The Results",{"type":19,"tag":28,"props":1094,"children":1095},{},[1096],{"type":25,"value":1097},"The migration shipped incrementally, with zero downtime, and the codebase came out the other side smaller, faster to build, and easier to type-check.",{"type":19,"tag":28,"props":1099,"children":1100},{},[1101],{"type":19,"tag":42,"props":1102,"children":1103},{},[1104],{"type":25,"value":1105},"What held up qualitatively:",{"type":19,"tag":50,"props":1107,"children":1108},{},[1109,1114,1126],{"type":19,"tag":38,"props":1110,"children":1111},{},[1112],{"type":25,"value":1113},"Tree-shaking finally worked, so dead code stopped shipping",{"type":19,"tag":38,"props":1115,"children":1116},{},[1117,1119,1124],{"type":25,"value":1118},"Full TypeScript inference replaced ",{"type":19,"tag":133,"props":1120,"children":1122},{"className":1121},[],[1123],{"type":25,"value":975},{"type":25,"value":1125}," scattered through the codebase",{"type":19,"tag":38,"props":1127,"children":1128},{},[1129],{"type":25,"value":1130},"Vite's dev server made the edit-refresh loop feel free again",{"type":19,"tag":20,"props":1132,"children":1134},{"id":1133},"lessons-for-your-codebase",[1135],{"type":25,"value":1136},"Lessons for Your Codebase",{"type":19,"tag":34,"props":1138,"children":1139},{},[1140,1150,1160,1170,1180],{"type":19,"tag":38,"props":1141,"children":1142},{},[1143,1148],{"type":19,"tag":42,"props":1144,"children":1145},{},[1146],{"type":25,"value":1147},"Tooling matters as much as code",{"type":25,"value":1149}," — Vite's superior DX made the whole migration feel faster",{"type":19,"tag":38,"props":1151,"children":1152},{},[1153,1158],{"type":19,"tag":42,"props":1154,"children":1155},{},[1156],{"type":25,"value":1157},"Go incremental",{"type":25,"value":1159}," — don't try a big bang rewrite. Parallel migration kept the team shipping",{"type":19,"tag":38,"props":1161,"children":1162},{},[1163,1168],{"type":19,"tag":42,"props":1164,"children":1165},{},[1166],{"type":25,"value":1167},"Type safety is an enabler",{"type":25,"value":1169}," — we spent 20% on migration, but gained 80% in error prevention",{"type":19,"tag":38,"props":1171,"children":1172},{},[1173,1178],{"type":19,"tag":42,"props":1174,"children":1175},{},[1176],{"type":25,"value":1177},"Test coverage reveals truth",{"type":25,"value":1179}," — broken tests weren't a migration problem; they revealed logic bugs",{"type":19,"tag":38,"props":1181,"children":1182},{},[1183,1188],{"type":19,"tag":42,"props":1184,"children":1185},{},[1186],{"type":25,"value":1187},"Plan for edge cases",{"type":25,"value":1189}," — our 10% \"final polish\" took 20% of the time (event handling, filters, etc.)",{"type":19,"tag":20,"props":1191,"children":1193},{"id":1192},"what-wed-do-differently",[1194],{"type":25,"value":1195},"What We'd Do Differently",{"type":19,"tag":50,"props":1197,"children":1198},{},[1199,1209,1219,1229],{"type":19,"tag":38,"props":1200,"children":1201},{},[1202,1207],{"type":19,"tag":42,"props":1203,"children":1204},{},[1205],{"type":25,"value":1206},"Start with Vite first",{"type":25,"value":1208}," — migrate tooling before components (the performance boost motivated the team)",{"type":19,"tag":38,"props":1210,"children":1211},{},[1212,1217],{"type":19,"tag":42,"props":1213,"children":1214},{},[1215],{"type":25,"value":1216},"Create better codemods",{"type":25,"value":1218}," — we wrote several after the fact that would have saved days",{"type":19,"tag":38,"props":1220,"children":1221},{},[1222,1227],{"type":19,"tag":42,"props":1223,"children":1224},{},[1225],{"type":25,"value":1226},"Allocate 15-20% for unknowns",{"type":25,"value":1228}," — we estimated 6 weeks; it took 8. That's normal for large migrations",{"type":19,"tag":38,"props":1230,"children":1231},{},[1232,1237],{"type":19,"tag":42,"props":1233,"children":1234},{},[1235],{"type":25,"value":1236},"Train junior engineers",{"type":25,"value":1238}," — force 2-3 people to migrate components solo. They become experts quickly",{"type":19,"tag":28,"props":1240,"children":1241},{},[1242],{"type":25,"value":1243},"The Vue 3 migration wasn't just about staying current. It fundamentally improved how we ship features, onboard engineers, and debug in production. If you're on the fence, the ROI is worth it.",{"title":7,"searchDepth":681,"depth":681,"links":1245},[1246,1247,1253,1254,1255],{"id":730,"depth":681,"text":733},{"id":801,"depth":681,"text":804,"children":1248},[1249,1250,1251,1252],{"id":807,"depth":687,"text":810},{"id":864,"depth":687,"text":867},{"id":978,"depth":687,"text":981},{"id":1059,"depth":687,"text":1062},{"id":1089,"depth":681,"text":1092},{"id":1133,"depth":681,"text":1136},{"id":1192,"depth":681,"text":1195},"content:blog:vue-2-to-vue-3-migration-at-scale.md","blog\u002Fvue-2-to-vue-3-migration-at-scale.md","blog\u002Fvue-2-to-vue-3-migration-at-scale",{"_path":1260,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1261,"description":1262,"date":1263,"updated":720,"readingTime":1264,"tags":1265,"featured":725,"body":1266,"_type":708,"_id":1819,"_source":710,"_file":1820,"_stem":1821,"_extension":713},"\u002Fblog\u002Fnuxt-3-performance-optimization-guide","Nuxt 3 Performance Optimization: A Production Playbook for Core Web Vitals","A production playbook for optimizing Nuxt 3 applications. SSR strategies, image optimization, code splitting, and Core Web Vitals tuning that holds up in CI.","2024-08-22",14,[14,13],{"type":16,"children":1267,"toc":1804},[1268,1274,1286,1300,1306,1312,1317,1326,1331,1364,1370,1375,1384,1394,1412,1418,1423,1432,1437,1446,1459,1468,1478,1484,1489,1498,1503,1509,1514,1523,1533,1544,1549,1555,1560,1569,1574,1592,1597,1603,1608,1617,1622,1628,1633,1687,1699,1705,1710,1719,1724,1742,1746,1799],{"type":19,"tag":20,"props":1269,"children":1271},{"id":1270},"starting-point-a-slow-production-app",[1272],{"type":25,"value":1273},"Starting Point: A Slow Production App",{"type":19,"tag":28,"props":1275,"children":1276},{},[1277,1279,1284],{"type":25,"value":1278},"The app this playbook comes from was slow in the way most B2B apps get slow — gradually, then noticeably. Largest Contentful Paint on key routes was ",{"type":19,"tag":42,"props":1280,"children":1281},{},[1282],{"type":25,"value":1283},"4.2s",{"type":25,"value":1285}," on throttled mobile. By Google's standards, anything over 2.5s for LCP is failing, and every user felt it.",{"type":19,"tag":28,"props":1287,"children":1288},{},[1289,1291,1298],{"type":25,"value":1290},"The full program behind this post — what was tried, what was rejected, and how the results are enforced — is written up in the ",{"type":19,"tag":1292,"props":1293,"children":1295},"a",{"href":1294},"\u002Fcase-studies\u002Fsaas-performance-program",[1296],{"type":25,"value":1297},"SaaS performance case study",{"type":25,"value":1299},". This post is the generalized playbook.",{"type":19,"tag":20,"props":1301,"children":1303},{"id":1302},"the-optimization-playbook",[1304],{"type":25,"value":1305},"The Optimization Playbook",{"type":19,"tag":121,"props":1307,"children":1309},{"id":1308},"_1-analyze-with-lighthouse-bundle-analysis",[1310],{"type":25,"value":1311},"1. Analyze with Lighthouse & Bundle Analysis",{"type":19,"tag":28,"props":1313,"children":1314},{},[1315],{"type":25,"value":1316},"Before optimizing, we measured everything:",{"type":19,"tag":128,"props":1318,"children":1321},{"className":1319,"code":1320,"language":156,"meta":7},[158],"npm run build --analyze\n# Generated visual bundle report\n\n# Lighthouse CI integration\nnpm install -D @lhci\u002Fcli@0.9.x\n",[1322],{"type":19,"tag":133,"props":1323,"children":1324},{"__ignoreMap":7},[1325],{"type":25,"value":1320},{"type":19,"tag":28,"props":1327,"children":1328},{},[1329],{"type":25,"value":1330},"A typical analysis of an unoptimized app reveals something like this (illustrative example — run your own):",{"type":19,"tag":50,"props":1332,"children":1333},{},[1334,1344,1354],{"type":19,"tag":38,"props":1335,"children":1336},{},[1337,1342],{"type":19,"tag":42,"props":1338,"children":1339},{},[1340],{"type":25,"value":1341},"Main bundle",{"type":25,"value":1343},": a few hundred KB gzipped of code the first paint doesn't need",{"type":19,"tag":38,"props":1345,"children":1346},{},[1347,1352],{"type":19,"tag":42,"props":1348,"children":1349},{},[1350],{"type":25,"value":1351},"Vendor bundle",{"type":25,"value":1353},": duplicate versions of the same utility library, bundled separately",{"type":19,"tag":38,"props":1355,"children":1356},{},[1357,1362],{"type":19,"tag":42,"props":1358,"children":1359},{},[1360],{"type":25,"value":1361},"Images",{"type":25,"value":1363},": the largest single line item — no WebP, no lazy loading, full-resolution on mobile",{"type":19,"tag":121,"props":1365,"children":1367},{"id":1366},"_2-code-splitting-strategy",[1368],{"type":25,"value":1369},"2. Code Splitting Strategy",{"type":19,"tag":28,"props":1371,"children":1372},{},[1373],{"type":25,"value":1374},"Nuxt 3 has better defaults than Nuxt 2, but we needed aggressive splitting:",{"type":19,"tag":128,"props":1376,"children":1379},{"className":1377,"code":1378,"language":172,"meta":7},[174],"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  build: {\n    rollupOptions: {\n      output: {\n        manualChunks: {\n          \u002F\u002F Separate heavy dependencies\n          'date-fns': ['date-fns'],\n          'lodash': ['lodash-es'],\n          'charts': ['chart.js', 'vue-chartjs'],\n          \u002F\u002F Keep vendor chunks under 200KB each\n        }\n      }\n    }\n  },\n  nitro: {\n    prerender: {\n      crawlLinks: true,\n      routes: ['\u002Fsitemap.xml', '\u002Frss.xml'],\n      \u002F\u002F Pre-render critical routes at build time\n      ignore: ['\u002Fadmin', '\u002Fpreview']\n    }\n  }\n})\n",[1380],{"type":19,"tag":133,"props":1381,"children":1382},{"__ignoreMap":7},[1383],{"type":25,"value":1378},{"type":19,"tag":28,"props":1385,"children":1386},{},[1387,1392],{"type":19,"tag":42,"props":1388,"children":1389},{},[1390],{"type":25,"value":1391},"What to expect",{"type":25,"value":1393}," (illustrative example — your numbers will differ):",{"type":19,"tag":50,"props":1395,"children":1396},{},[1397,1402,1407],{"type":19,"tag":38,"props":1398,"children":1399},{},[1400],{"type":25,"value":1401},"The main bundle drops to whatever the first route genuinely needs",{"type":19,"tag":38,"props":1403,"children":1404},{},[1405],{"type":25,"value":1406},"Each lazy route loads a small chunk on demand",{"type":19,"tag":38,"props":1408,"children":1409},{},[1410],{"type":25,"value":1411},"Routes render without waiting for full app initialization",{"type":19,"tag":121,"props":1413,"children":1415},{"id":1414},"_3-image-optimization",[1416],{"type":25,"value":1417},"3. Image Optimization",{"type":19,"tag":28,"props":1419,"children":1420},{},[1421],{"type":25,"value":1422},"This single change had the biggest impact:",{"type":19,"tag":128,"props":1424,"children":1427},{"className":1425,"code":1426,"language":297,"meta":7},[299],"\u003Cscript setup lang=\"ts\">\nimport { useImage } from '#app'\n\nconst imageUrl = 'hero.jpg'\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ❌ Old way: no optimization -->\n  \u003Cimg src=\"\u002Fimages\u002Fhero.jpg\" alt=\"Hero\" \u002F>\n  \n  \u003C!-- ✅ New way: Nuxt Image component -->\n  \u003CNuxtImg\n    src=\"\u002Fimages\u002Fhero.jpg\"\n    alt=\"Hero\"\n    sizes=\"xs:100vw sm:100vw md:600px lg:800px\"\n    format=\"webp,jpg\"\n    quality=\"80\"\n    loading=\"lazy\"\n  \u002F>\n\u003C\u002Ftemplate>\n",[1428],{"type":19,"tag":133,"props":1429,"children":1430},{"__ignoreMap":7},[1431],{"type":25,"value":1426},{"type":19,"tag":28,"props":1433,"children":1434},{},[1435],{"type":25,"value":1436},"Install the Nuxt Image module:",{"type":19,"tag":128,"props":1438,"children":1441},{"className":1439,"code":1440,"language":156,"meta":7},[158],"npm install @nuxt\u002Fimage\n",[1442],{"type":19,"tag":133,"props":1443,"children":1444},{"__ignoreMap":7},[1445],{"type":25,"value":1440},{"type":19,"tag":28,"props":1447,"children":1448},{},[1449,1451,1457],{"type":25,"value":1450},"Configure in ",{"type":19,"tag":133,"props":1452,"children":1454},{"className":1453},[],[1455],{"type":25,"value":1456},"nuxt.config.ts",{"type":25,"value":1458},":",{"type":19,"tag":128,"props":1460,"children":1463},{"className":1461,"code":1462,"language":172,"meta":7},[174],"export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fimage'],\n  image: {\n    provider: 'ipx', \u002F\u002F or 'cloudinary', 'imgix', etc.\n    ipx: {\n      maxAge: 60 * 60 * 24 * 365, \u002F\u002F 1 year cache\n    }\n  }\n})\n",[1464],{"type":19,"tag":133,"props":1465,"children":1466},{"__ignoreMap":7},[1467],{"type":25,"value":1462},{"type":19,"tag":28,"props":1469,"children":1470},{},[1471,1476],{"type":19,"tag":42,"props":1472,"children":1473},{},[1474],{"type":25,"value":1475},"Why this matters most",{"type":25,"value":1477},": on image-heavy routes, WebP conversion plus lazy loading is routinely the single largest LCP win — images are usually the largest contentful paint element. Measure your image bytes before and after; the reduction is typically dramatic.",{"type":19,"tag":121,"props":1479,"children":1481},{"id":1480},"_4-server-side-rendering-ssr-optimization",[1482],{"type":25,"value":1483},"4. Server-Side Rendering (SSR) Optimization",{"type":19,"tag":28,"props":1485,"children":1486},{},[1487],{"type":25,"value":1488},"Nuxt 3's SSR is better, but configuration matters:",{"type":19,"tag":128,"props":1490,"children":1493},{"className":1491,"code":1492,"language":172,"meta":7},[174],"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  nitro: {\n    \u002F\u002F Compress responses\n    gzip: { enabled: true },\n    brotli: { enabled: true },\n    \n    \u002F\u002F Cache aggressively\n    headers: {\n      'Cache-Control': 'public, max-age=3600, s-maxage=86400'\n    },\n    \n    \u002F\u002F Pre-render static routes\n    prerender: {\n      crawlLinks: true,\n      routes: ['\u002Fblog', '\u002Fprojects', '\u002F'],\n      ignore: ['\u002Fadmin', '\u002Fapi\u002Finternal']\n    },\n    \n    \u002F\u002F Enable Redis caching for API responses\n    storage: {\n      redis: {\n        host: process.env.REDIS_HOST,\n        port: 6379\n      }\n    }\n  }\n})\n",[1494],{"type":19,"tag":133,"props":1495,"children":1496},{"__ignoreMap":7},[1497],{"type":25,"value":1492},{"type":19,"tag":28,"props":1499,"children":1500},{},[1501],{"type":25,"value":1502},"The result: time-to-first-byte drops sharply once cacheable responses stop hitting the origin (illustrative — TTFB depends heavily on your hosting).",{"type":19,"tag":121,"props":1504,"children":1506},{"id":1505},"_5-font-loading-optimization",[1507],{"type":25,"value":1508},"5. Font Loading Optimization",{"type":19,"tag":28,"props":1510,"children":1511},{},[1512],{"type":25,"value":1513},"Fonts are invisible but heavy. We optimized aggressively:",{"type":19,"tag":128,"props":1515,"children":1518},{"className":1516,"code":1517,"language":172,"meta":7},[174],"\u002F\u002F app.vue\nimport { useState } from '#app'\n\nconst fontLoadingComplete = useState('fontLoadingComplete', () => false)\n\nuseHead({\n  link: [\n    \u002F\u002F Preconnect to font providers\n    {\n      rel: 'preconnect',\n      href: 'https:\u002F\u002Ffonts.googleapis.com'\n    },\n    {\n      rel: 'preconnect',\n      href: 'https:\u002F\u002Ffonts.gstatic.com',\n      crossorigin: ''\n    },\n    \u002F\u002F Load only necessary weights\n    {\n      rel: 'stylesheet',\n      href: 'https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap'\n    }\n  ]\n})\n",[1519],{"type":19,"tag":133,"props":1520,"children":1521},{"__ignoreMap":7},[1522],{"type":25,"value":1517},{"type":19,"tag":28,"props":1524,"children":1525},{},[1526,1528,1532],{"type":25,"value":1527},"But the real win came from ",{"type":19,"tag":42,"props":1529,"children":1530},{},[1531],{"type":25,"value":410},{"type":25,"value":1458},{"type":19,"tag":128,"props":1534,"children":1539},{"className":1535,"code":1537,"language":1538,"meta":7},[1536],"language-css","@import url('https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap');\n","css",[1540],{"type":19,"tag":133,"props":1541,"children":1542},{"__ignoreMap":7},[1543],{"type":25,"value":1537},{"type":19,"tag":28,"props":1545,"children":1546},{},[1547],{"type":25,"value":1548},"This tells the browser: \"Show text immediately with fallback, swap in custom font when ready.\" No invisible text delays.",{"type":19,"tag":121,"props":1550,"children":1552},{"id":1551},"_6-hydration-optimization",[1553],{"type":25,"value":1554},"6. Hydration Optimization",{"type":19,"tag":28,"props":1556,"children":1557},{},[1558],{"type":25,"value":1559},"This is a Nuxt 3 secret weapon. Partial hydration means not every component needs JavaScript:",{"type":19,"tag":128,"props":1561,"children":1564},{"className":1562,"code":1563,"language":297,"meta":7},[299],"\u003C!-- This component renders as static HTML on server -->\n\u003C!-- JavaScript only loads when needed -->\n\u003CClientOnly fallback=\"\u003Cdiv class='skeleton'>\u003C\u002Fdiv>\">\n  \u003CExpensiveInteractiveComponent \u002F>\n\u003C\u002FClientOnly>\n",[1565],{"type":19,"tag":133,"props":1566,"children":1567},{"__ignoreMap":7},[1568],{"type":25,"value":1563},{"type":19,"tag":28,"props":1570,"children":1571},{},[1572],{"type":25,"value":1573},"Use it for:",{"type":19,"tag":50,"props":1575,"children":1576},{},[1577,1582,1587],{"type":19,"tag":38,"props":1578,"children":1579},{},[1580],{"type":25,"value":1581},"Hero sections (no JS needed)",{"type":19,"tag":38,"props":1583,"children":1584},{},[1585],{"type":25,"value":1586},"Blog content (pure markup)",{"type":19,"tag":38,"props":1588,"children":1589},{},[1590],{"type":25,"value":1591},"Static sidebars",{"type":19,"tag":28,"props":1593,"children":1594},{},[1595],{"type":25,"value":1596},"Static sections that skip hydration are JavaScript the browser never has to parse.",{"type":19,"tag":121,"props":1598,"children":1600},{"id":1599},"_7-caching-strategy",[1601],{"type":25,"value":1602},"7. Caching Strategy",{"type":19,"tag":28,"props":1604,"children":1605},{},[1606],{"type":25,"value":1607},"Implemented a tiered caching approach:",{"type":19,"tag":128,"props":1609,"children":1612},{"className":1610,"code":1611,"language":172,"meta":7},[174],"\u002F\u002F server\u002Fapi\u002Fposts.get.ts\nexport default cachedEventHandler(\n  async (event) => {\n    return await db.posts.findAll()\n  },\n  {\n    maxAge: 60 * 10, \u002F\u002F 10 minute cache\n    sMaxAge: 60 * 60 * 24, \u002F\u002F 1 day for CDN\n    staleMaxAge: 60 * 60 * 24 * 7, \u002F\u002F 1 week stale\n    name: 'api_posts',\n    getKey: () => 'api_posts',\n    vary: ['accept-encoding'] \u002F\u002F Cache based on compression\n  }\n)\n",[1613],{"type":19,"tag":133,"props":1614,"children":1615},{"__ignoreMap":7},[1616],{"type":25,"value":1611},{"type":19,"tag":28,"props":1618,"children":1619},{},[1620],{"type":25,"value":1621},"With a tiered cache, most repeat API reads never reach the database.",{"type":19,"tag":20,"props":1623,"children":1625},{"id":1624},"the-final-results",[1626],{"type":25,"value":1627},"The Final Results",{"type":19,"tag":28,"props":1629,"children":1630},{},[1631],{"type":25,"value":1632},"On the production program this playbook comes from, the headline result was:",{"type":19,"tag":478,"props":1634,"children":1635},{},[1636,1659],{"type":19,"tag":482,"props":1637,"children":1638},{},[1639],{"type":19,"tag":486,"props":1640,"children":1641},{},[1642,1646,1650,1654],{"type":19,"tag":490,"props":1643,"children":1644},{},[1645],{"type":25,"value":494},{"type":19,"tag":490,"props":1647,"children":1648},{},[1649],{"type":25,"value":499},{"type":19,"tag":490,"props":1651,"children":1652},{},[1653],{"type":25,"value":504},{"type":19,"tag":490,"props":1655,"children":1656},{},[1657],{"type":25,"value":1658},"Improvement",{"type":19,"tag":506,"props":1660,"children":1661},{},[1662],{"type":19,"tag":486,"props":1663,"children":1664},{},[1665,1670,1674,1679],{"type":19,"tag":513,"props":1666,"children":1667},{},[1668],{"type":25,"value":1669},"Largest Contentful Paint",{"type":19,"tag":513,"props":1671,"children":1672},{},[1673],{"type":25,"value":1283},{"type":19,"tag":513,"props":1675,"children":1676},{},[1677],{"type":25,"value":1678},"2.5s",{"type":19,"tag":513,"props":1680,"children":1681},{},[1682],{"type":19,"tag":42,"props":1683,"children":1684},{},[1685],{"type":25,"value":1686},"−40%",{"type":19,"tag":28,"props":1688,"children":1689},{},[1690,1692,1697],{"type":25,"value":1691},"The other vitals moved in the same direction; the LCP number is the one measured carefully enough to publish. The ",{"type":19,"tag":1292,"props":1693,"children":1694},{"href":1294},[1695],{"type":25,"value":1696},"case study",{"type":25,"value":1698}," covers how it was measured and enforced.",{"type":19,"tag":20,"props":1700,"children":1702},{"id":1701},"performance-budget-going-forward",[1703],{"type":25,"value":1704},"Performance Budget Going Forward",{"type":19,"tag":28,"props":1706,"children":1707},{},[1708],{"type":25,"value":1709},"We set strict budgets to prevent regressions:",{"type":19,"tag":128,"props":1711,"children":1714},{"className":1712,"code":1713,"language":156,"meta":7},[158],"# package.json\n\"build-analyze\": \"nuxi build && npm run bundle-check\",\n\n# .github\u002Fworkflows\u002Fperformance.yml\n- name: Bundle Size Check\n  run: npm run build-analyze\n  # Fails if bundles exceed thresholds\n",[1715],{"type":19,"tag":133,"props":1716,"children":1717},{"__ignoreMap":7},[1718],{"type":25,"value":1713},{"type":19,"tag":28,"props":1720,"children":1721},{},[1722],{"type":25,"value":1723},"Thresholds:",{"type":19,"tag":50,"props":1725,"children":1726},{},[1727,1732,1737],{"type":19,"tag":38,"props":1728,"children":1729},{},[1730],{"type":25,"value":1731},"Main bundle: \u003C 200KB gzipped",{"type":19,"tag":38,"props":1733,"children":1734},{},[1735],{"type":25,"value":1736},"Per route: \u003C 80KB gzipped",{"type":19,"tag":38,"props":1738,"children":1739},{},[1740],{"type":25,"value":1741},"Images: \u003C 1MB total per page",{"type":19,"tag":20,"props":1743,"children":1744},{"id":618},[1745],{"type":25,"value":621},{"type":19,"tag":34,"props":1747,"children":1748},{},[1749,1759,1769,1779,1789],{"type":19,"tag":38,"props":1750,"children":1751},{},[1752,1757],{"type":19,"tag":42,"props":1753,"children":1754},{},[1755],{"type":25,"value":1756},"Measure first",{"type":25,"value":1758}," — every optimization should be data-driven",{"type":19,"tag":38,"props":1760,"children":1761},{},[1762,1767],{"type":19,"tag":42,"props":1763,"children":1764},{},[1765],{"type":25,"value":1766},"Images are the lowest-hanging fruit",{"type":25,"value":1768}," — image handling routinely delivers the largest share of the wins",{"type":19,"tag":38,"props":1770,"children":1771},{},[1772,1777],{"type":19,"tag":42,"props":1773,"children":1774},{},[1775],{"type":25,"value":1776},"HTTP caching > code optimization",{"type":25,"value":1778}," — smart caching beats micro-optimizations",{"type":19,"tag":38,"props":1780,"children":1781},{},[1782,1787],{"type":19,"tag":42,"props":1783,"children":1784},{},[1785],{"type":25,"value":1786},"Nuxt 3's primitives are powerful",{"type":25,"value":1788}," — when used correctly, you don't need complex workarounds",{"type":19,"tag":38,"props":1790,"children":1791},{},[1792,1797],{"type":19,"tag":42,"props":1793,"children":1794},{},[1795],{"type":25,"value":1796},"Monitor in production",{"type":25,"value":1798}," — real-world metrics differ from labs. Use tools like Sentry, Vercel Analytics",{"type":19,"tag":28,"props":1800,"children":1801},{},[1802],{"type":25,"value":1803},"If you're on Nuxt 2, the move to Nuxt 3 alone usually buys a meaningful improvement before you optimize anything — better defaults, better tree-shaking, better hydration. The playbook above is how you claim the rest.",{"title":7,"searchDepth":681,"depth":681,"links":1805},[1806,1807,1816,1817,1818],{"id":1270,"depth":681,"text":1273},{"id":1302,"depth":681,"text":1305,"children":1808},[1809,1810,1811,1812,1813,1814,1815],{"id":1308,"depth":687,"text":1311},{"id":1366,"depth":687,"text":1369},{"id":1414,"depth":687,"text":1417},{"id":1480,"depth":687,"text":1483},{"id":1505,"depth":687,"text":1508},{"id":1551,"depth":687,"text":1554},{"id":1599,"depth":687,"text":1602},{"id":1624,"depth":681,"text":1627},{"id":1701,"depth":681,"text":1704},{"id":618,"depth":681,"text":621},"content:blog:nuxt-3-performance-optimization-guide.md","blog\u002Fnuxt-3-performance-optimization-guide.md","blog\u002Fnuxt-3-performance-optimization-guide",{"_path":1823,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1824,"description":1825,"date":1826,"updated":720,"readingTime":1827,"tags":1828,"featured":6,"body":1829,"_type":708,"_id":2364,"_source":710,"_file":2365,"_stem":2366,"_extension":713},"\u002Fblog\u002Fbuilding-scalable-design-systems-vue","Building Scalable Design Systems in Vue: Tokens, APIs, and Versioning That Scale","How to architect a design system that scales across teams. Learn component APIs, token management, versioning strategies, and Storybook workflows that actually work.","2024-07-30",13,[723,724],{"type":16,"children":1830,"toc":2350},[1831,1837,1842,1847,1870,1883,1889,1895,1900,1908,1913,1936,1942,1947,1956,1961,2004,2010,2015,2024,2029,2052,2058,2063,2069,2078,2084,2093,2099,2104,2113,2119,2124,2133,2138,2171,2176,2187,2193,2205,2258,2264,2276,2282,2345],{"type":19,"tag":20,"props":1832,"children":1834},{"id":1833},"the-problem-with-ad-hoc-components",[1835],{"type":25,"value":1836},"The Problem with Ad-Hoc Components",{"type":19,"tag":28,"props":1838,"children":1839},{},[1840],{"type":25,"value":1841},"We started with a few buttons and modals in a shared folder. A few years later, components were spread across repositories, with competing ways to handle form validation, competing naming conventions, and constant debates about \"is this a Button or a PrimaryButton?\"",{"type":19,"tag":28,"props":1843,"children":1844},{},[1845],{"type":25,"value":1846},"Inconsistency was costing us:",{"type":19,"tag":50,"props":1848,"children":1849},{},[1850,1855,1860,1865],{"type":19,"tag":38,"props":1851,"children":1852},{},[1853],{"type":25,"value":1854},"New team members took weeks to learn existing patterns",{"type":19,"tag":38,"props":1856,"children":1857},{},[1858],{"type":25,"value":1859},"Components were re-implemented multiple times across products",{"type":19,"tag":38,"props":1861,"children":1862},{},[1863],{"type":25,"value":1864},"No single source of truth for design decisions",{"type":19,"tag":38,"props":1866,"children":1867},{},[1868],{"type":25,"value":1869},"Design-to-dev handoff took meetings just to clarify prop names",{"type":19,"tag":28,"props":1871,"children":1872},{},[1873,1875,1881],{"type":25,"value":1874},"We needed a design system. The production version of this story — with the monorepo layout, versioning policy, and adoption mechanics — is in the ",{"type":19,"tag":1292,"props":1876,"children":1878},{"href":1877},"\u002Fcase-studies\u002Fmonorepo-design-system",[1879],{"type":25,"value":1880},"monorepo design system case study",{"type":25,"value":1882},".",{"type":19,"tag":20,"props":1884,"children":1886},{"id":1885},"architecture-the-foundation",[1887],{"type":25,"value":1888},"Architecture: The Foundation",{"type":19,"tag":121,"props":1890,"children":1892},{"id":1891},"_1-monorepo-structure",[1893],{"type":25,"value":1894},"1. Monorepo Structure",{"type":19,"tag":28,"props":1896,"children":1897},{},[1898],{"type":25,"value":1899},"We used pnpm workspaces:",{"type":19,"tag":128,"props":1901,"children":1903},{"code":1902},"packages\u002F\n├── @acme\u002Fdesign-system\u002F\n│   ├── src\u002F\n│   │   ├── components\u002F\n│   │   │   ├── Button\u002F\n│   │   │   │   ├── Button.vue\n│   │   │   │   ├── Button.test.ts\n│   │   │   │   ├── Button.stories.ts\n│   │   │   │   └── index.ts\n│   │   │   ├── Input\u002F\n│   │   │   └── ...\n│   │   ├── styles\u002F\n│   │   │   ├── tokens.css\n│   │   │   ├── base.css\n│   │   │   └── utilities.css\n│   │   └── index.ts\n│   ├── package.json\n│   └── tsconfig.json\n├── @acme\u002Ficons\u002F\n├── @acme\u002Fdocs\u002F\n└── @acme\u002Fbrand\u002F\n",[1904],{"type":19,"tag":133,"props":1905,"children":1906},{"__ignoreMap":7},[1907],{"type":25,"value":1902},{"type":19,"tag":28,"props":1909,"children":1910},{},[1911],{"type":25,"value":1912},"Benefits:",{"type":19,"tag":50,"props":1914,"children":1915},{},[1916,1921,1926,1931],{"type":19,"tag":38,"props":1917,"children":1918},{},[1919],{"type":25,"value":1920},"Monorepo enforces consistency",{"type":19,"tag":38,"props":1922,"children":1923},{},[1924],{"type":25,"value":1925},"Shared tooling configuration",{"type":19,"tag":38,"props":1927,"children":1928},{},[1929],{"type":25,"value":1930},"Easy to test components in isolation",{"type":19,"tag":38,"props":1932,"children":1933},{},[1934],{"type":25,"value":1935},"Clear dependency management",{"type":19,"tag":121,"props":1937,"children":1939},{"id":1938},"_2-component-api-design",[1940],{"type":25,"value":1941},"2. Component API Design",{"type":19,"tag":28,"props":1943,"children":1944},{},[1945],{"type":25,"value":1946},"We established strict rules for component props:",{"type":19,"tag":128,"props":1948,"children":1951},{"code":1949,"language":172,"meta":7,"className":1950},"\u002F\u002F ❌ Bad: Too many variants\ninterface ButtonProps {\n  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n  variant?: 'primary' | 'secondary' | 'danger' | 'warning' | 'info'\n  disabled?: boolean\n  loading?: boolean\n  fullWidth?: boolean\n}\n\n\u002F\u002F ✅ Good: Composition over props\ninterface ButtonProps {\n  variant?: 'primary' | 'secondary' | 'danger'\n  size?: 'sm' | 'md' | 'lg'\n  disabled?: boolean\n  loading?: boolean\n}\n\ninterface ButtonGroupProps {\n  orientation?: 'horizontal' | 'vertical'\n}\n\n\u002F\u002F Compose complex UIs from simple components\n",[174],[1952],{"type":19,"tag":133,"props":1953,"children":1954},{"__ignoreMap":7},[1955],{"type":25,"value":1949},{"type":19,"tag":28,"props":1957,"children":1958},{},[1959],{"type":25,"value":1960},"Guidelines:",{"type":19,"tag":34,"props":1962,"children":1963},{},[1964,1974,1984,1994],{"type":19,"tag":38,"props":1965,"children":1966},{},[1967,1972],{"type":19,"tag":42,"props":1968,"children":1969},{},[1970],{"type":25,"value":1971},"Keep props count \u003C 8",{"type":25,"value":1973}," — if you need more, you're probably missing a abstraction",{"type":19,"tag":38,"props":1975,"children":1976},{},[1977,1982],{"type":19,"tag":42,"props":1978,"children":1979},{},[1980],{"type":25,"value":1981},"Use CSS variables for styling",{"type":25,"value":1983}," — don't expose every style property",{"type":19,"tag":38,"props":1985,"children":1986},{},[1987,1992],{"type":19,"tag":42,"props":1988,"children":1989},{},[1990],{"type":25,"value":1991},"Provide sensible defaults",{"type":25,"value":1993}," — most components should work with zero props",{"type":19,"tag":38,"props":1995,"children":1996},{},[1997,2002],{"type":19,"tag":42,"props":1998,"children":1999},{},[2000],{"type":25,"value":2001},"Document the \"why\"",{"type":25,"value":2003}," — every prop should have a reason",{"type":19,"tag":121,"props":2005,"children":2007},{"id":2006},"_3-design-tokens",[2008],{"type":25,"value":2009},"3. Design Tokens",{"type":19,"tag":28,"props":2011,"children":2012},{},[2013],{"type":25,"value":2014},"Tokens are the single source of truth:",{"type":19,"tag":128,"props":2016,"children":2019},{"code":2017,"language":172,"meta":7,"className":2018},"\u002F\u002F src\u002Fstyles\u002Ftokens.ts\nexport const tokens = {\n  colors: {\n    primary: '#0070F3',\n    secondary: '#666',\n    danger: '#E81E3D',\n    success: '#21BA45',\n  },\n  spacing: {\n    xs: '0.25rem',\n    sm: '0.5rem',\n    md: '1rem',\n    lg: '1.5rem',\n    xl: '2rem',\n  },\n  typography: {\n    body: { size: '1rem', weight: 400, lineHeight: 1.5 },\n    heading: { size: '1.5rem', weight: 600, lineHeight: 1.2 },\n  },\n  radius: {\n    sm: '0.25rem',\n    md: '0.5rem',\n    lg: '1rem',\n  },\n  shadow: {\n    sm: '0 1px 2px rgba(0,0,0,0.05)',\n    md: '0 4px 6px rgba(0,0,0,0.1)',\n    lg: '0 10px 15px rgba(0,0,0,0.2)',\n  },\n}\n\n\u002F\u002F Generate CSS variables\nconst generateCSSVariables = (tokens: any, prefix = '') => {\n  let css = ':root {\\n'\n  Object.entries(tokens).forEach(([key, value]) => {\n    if (typeof value === 'object') {\n      css += generateCSSVariables(value, `${prefix}${key}-`)\n    } else {\n      css += `  --${prefix}${key}: ${value};\\n`\n    }\n  })\n  css += '}\\n'\n  return css\n}\n",[174],[2020],{"type":19,"tag":133,"props":2021,"children":2022},{"__ignoreMap":7},[2023],{"type":25,"value":2017},{"type":19,"tag":28,"props":2025,"children":2026},{},[2027],{"type":25,"value":2028},"This single file becomes the source of truth for:",{"type":19,"tag":50,"props":2030,"children":2031},{},[2032,2037,2042,2047],{"type":19,"tag":38,"props":2033,"children":2034},{},[2035],{"type":25,"value":2036},"Figma design tokens",{"type":19,"tag":38,"props":2038,"children":2039},{},[2040],{"type":25,"value":2041},"CSS variables",{"type":19,"tag":38,"props":2043,"children":2044},{},[2045],{"type":25,"value":2046},"Type definitions",{"type":19,"tag":38,"props":2048,"children":2049},{},[2050],{"type":25,"value":2051},"Storybook theming",{"type":19,"tag":121,"props":2053,"children":2055},{"id":2054},"_4-component-patterns",[2056],{"type":25,"value":2057},"4. Component Patterns",{"type":19,"tag":28,"props":2059,"children":2060},{},[2061],{"type":25,"value":2062},"We established patterns for common scenarios:",{"type":19,"tag":907,"props":2064,"children":2066},{"id":2065},"simple-component",[2067],{"type":25,"value":2068},"Simple Component",{"type":19,"tag":128,"props":2070,"children":2073},{"code":2071,"language":297,"meta":7,"className":2072},"\u003Cscript setup lang=\"ts\">\ninterface Props {\n  variant?: 'primary' | 'secondary'\n  size?: 'sm' | 'md' | 'lg'\n  disabled?: boolean\n}\n\nwithDefaults(defineProps\u003CProps>(), {\n  variant: 'primary',\n  size: 'md',\n})\n\ndefineEmits\u003C{\n  click: [event: MouseEvent]\n}>()\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton\n    :class=\"[\n      'btn',\n      `btn-${variant}`,\n      `btn-${size}`,\n      { 'is-disabled': disabled }\n    ]\"\n    :disabled=\"disabled\"\n  >\n    \u003Cslot \u002F>\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.btn {\n  --py: var(--spacing-md);\n  --px: var(--spacing-lg);\n  padding: var(--py) var(--px);\n  background: var(--color-primary);\n  border-radius: var(--radius-md);\n  transition: all 0.2s ease;\n}\n\n.btn:hover:not(.is-disabled) {\n  background: var(--color-primary-dark);\n}\n\u003C\u002Fstyle>\n",[299],[2074],{"type":19,"tag":133,"props":2075,"children":2076},{"__ignoreMap":7},[2077],{"type":25,"value":2071},{"type":19,"tag":907,"props":2079,"children":2081},{"id":2080},"compound-component",[2082],{"type":25,"value":2083},"Compound Component",{"type":19,"tag":128,"props":2085,"children":2088},{"code":2086,"language":297,"meta":7,"className":2087},"\u003C!-- FieldGroup.vue -->\n\u003Cscript setup lang=\"ts\">\ninterface Props {\n  label?: string\n  error?: string\n  hint?: string\n}\n\nconst props = withDefaults(defineProps\u003CProps>(), {})\nconst id = `field-${Math.random().toString(36).slice(2, 9)}`\n\nprovide('fieldId', id)\nprovide('fieldError', props.error)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cfieldset class=\"field-group\">\n    \u003Clabel v-if=\"label\" :for=\"id\" class=\"field-label\">\n      {{ label }}\n    \u003C\u002Flabel>\n    \u003Cslot \u002F>\n    \u003Cp v-if=\"error\" :id=\"`${id}-error`\" class=\"field-error\">{{ error }}\u003C\u002Fp>\n    \u003Cp v-if=\"hint && !error\" :id=\"`${id}-hint`\" class=\"field-hint\">{{ hint }}\u003C\u002Fp>\n  \u003C\u002Ffieldset>\n\u003C\u002Ftemplate>\n\n\u003C!-- Usage -->\n\u003CFieldGroup label=\"Email\" error=\"Invalid email\">\n  \u003Cinput type=\"email\" required \u002F>\n\u003C\u002FFieldGroup>\n",[299],[2089],{"type":19,"tag":133,"props":2090,"children":2091},{"__ignoreMap":7},[2092],{"type":25,"value":2086},{"type":19,"tag":20,"props":2094,"children":2096},{"id":2095},"documentation-storybook",[2097],{"type":25,"value":2098},"Documentation & Storybook",{"type":19,"tag":28,"props":2100,"children":2101},{},[2102],{"type":25,"value":2103},"Storybook is critical for design system success:",{"type":19,"tag":128,"props":2105,"children":2108},{"code":2106,"language":172,"meta":7,"className":2107},"\u002F\u002F Button.stories.ts\nimport type { Meta, StoryObj } from '@storybook\u002Fvue3'\nimport Button from '.\u002FButton.vue'\n\nconst meta: Meta\u003Ctypeof Button> = {\n  component: Button,\n  tags: ['autodocs'],\n  argTypes: {\n    variant: {\n      options: ['primary', 'secondary', 'danger'],\n      control: { type: 'select' }\n    },\n    size: {\n      options: ['sm', 'md', 'lg'],\n      control: { type: 'select' }\n    }\n  }\n}\n\nexport default meta\n\nexport const Primary: StoryObj = {\n  args: {\n    variant: 'primary',\n    size: 'md'\n  },\n  render: (args) => ({\n    components: { Button },\n    setup() { return { args } },\n    template: '\u003CButton v-bind=\"args\">Click me\u003C\u002FButton>'\n  })\n}\n\nexport const Disabled: StoryObj = {\n  args: { disabled: true }\n}\n\nexport const All: StoryObj = {\n  render: () => ({\n    template: `\n      \u003Cdiv class=\"space-y-4\">\n        \u003CButton variant=\"primary\">Primary\u003C\u002FButton>\n        \u003CButton variant=\"secondary\">Secondary\u003C\u002FButton>\n        \u003CButton variant=\"danger\">Danger\u003C\u002FButton>\n      \u003C\u002Fdiv>\n    `\n  })\n}\n",[174],[2109],{"type":19,"tag":133,"props":2110,"children":2111},{"__ignoreMap":7},[2112],{"type":25,"value":2106},{"type":19,"tag":20,"props":2114,"children":2116},{"id":2115},"versioning-releases",[2117],{"type":25,"value":2118},"Versioning & Releases",{"type":19,"tag":28,"props":2120,"children":2121},{},[2122],{"type":25,"value":2123},"Design systems are libraries. Treat them like it:",{"type":19,"tag":128,"props":2125,"children":2128},{"code":2126,"language":156,"meta":7,"className":2127},"# semantic-release configuration\nnpm install -D semantic-release @semantic-release\u002Fcommit-analyzer @semantic-release\u002Frelease-notes-generator\n\n# Automatically bump versions based on commits\n# BREAKING CHANGE: → major\n# feat: → minor\n# fix: → patch\n",[158],[2129],{"type":19,"tag":133,"props":2130,"children":2131},{"__ignoreMap":7},[2132],{"type":25,"value":2126},{"type":19,"tag":28,"props":2134,"children":2135},{},[2136],{"type":25,"value":2137},"We published:",{"type":19,"tag":50,"props":2139,"children":2140},{},[2141,2151,2161],{"type":19,"tag":38,"props":2142,"children":2143},{},[2144,2149],{"type":19,"tag":42,"props":2145,"children":2146},{},[2147],{"type":25,"value":2148},"v1.0.0",{"type":25,"value":2150}," — the core component set\n",{"type":19,"tag":38,"props":2152,"children":2153},{},[2154,2159],{"type":19,"tag":42,"props":2155,"children":2156},{},[2157],{"type":25,"value":2158},"v1.2.3",{"type":25,"value":2160}," — new tokens, backward compatible",{"type":19,"tag":38,"props":2162,"children":2163},{},[2164,2169],{"type":19,"tag":42,"props":2165,"children":2166},{},[2167],{"type":25,"value":2168},"v2.0.0",{"type":25,"value":2170}," — redesign with breaking changes",{"type":19,"tag":28,"props":2172,"children":2173},{},[2174],{"type":25,"value":2175},"Each version went to npm registry. Teams pinned versions:",{"type":19,"tag":128,"props":2177,"children":2182},{"code":2178,"language":2179,"meta":7,"className":2180},"{\n  \"dependencies\": {\n    \"@acme\u002Fdesign-system\": \"^1.2.0\"\n  }\n}\n","json",[2181],"language-json",[2183],{"type":19,"tag":133,"props":2184,"children":2185},{"__ignoreMap":7},[2186],{"type":25,"value":2178},{"type":19,"tag":20,"props":2188,"children":2190},{"id":2189},"adoption-strategy",[2191],{"type":25,"value":2192},"Adoption Strategy",{"type":19,"tag":28,"props":2194,"children":2195},{},[2196,2198,2203],{"type":25,"value":2197},"Getting teams to ",{"type":19,"tag":988,"props":2199,"children":2200},{},[2201],{"type":25,"value":2202},"use",{"type":25,"value":2204}," the system was as important as building it:",{"type":19,"tag":34,"props":2206,"children":2207},{},[2208,2218,2228,2238,2248],{"type":19,"tag":38,"props":2209,"children":2210},{},[2211,2216],{"type":19,"tag":42,"props":2212,"children":2213},{},[2214],{"type":25,"value":2215},"Dogfood internally",{"type":25,"value":2217}," — our flagship product used v1.0 before public release",{"type":19,"tag":38,"props":2219,"children":2220},{},[2221,2226],{"type":19,"tag":42,"props":2222,"children":2223},{},[2224],{"type":25,"value":2225},"Create migration guides",{"type":25,"value":2227}," — \"How to replace your Button with DesignSystemButton\"",{"type":19,"tag":38,"props":2229,"children":2230},{},[2231,2236],{"type":19,"tag":42,"props":2232,"children":2233},{},[2234],{"type":25,"value":2235},"Celebrate wins",{"type":25,"value":2237}," — \"we deleted three duplicate Button implementations this sprint\" beats any adoption mandate",{"type":19,"tag":38,"props":2239,"children":2240},{},[2241,2246],{"type":19,"tag":42,"props":2242,"children":2243},{},[2244],{"type":25,"value":2245},"Make it easier to use than not",{"type":25,"value":2247}," — publish to npm, show install in docs",{"type":19,"tag":38,"props":2249,"children":2250},{},[2251,2256],{"type":19,"tag":42,"props":2252,"children":2253},{},[2254],{"type":25,"value":2255},"Maintain it publicly",{"type":25,"value":2257}," — commit to performance, accessibility, TypeScript support",{"type":19,"tag":20,"props":2259,"children":2261},{"id":2260},"adoption-metrics",[2262],{"type":25,"value":2263},"Adoption Metrics",{"type":19,"tag":28,"props":2265,"children":2266},{},[2267,2269,2274],{"type":25,"value":2268},"The measured outcome on the production system this post draws from: ",{"type":19,"tag":42,"props":2270,"children":2271},{},[2272],{"type":25,"value":2273},"20% faster feature delivery",{"type":25,"value":2275}," on projects consuming the library — teams assemble features from documented components instead of rebuilding primitives.",{"type":19,"tag":20,"props":2277,"children":2279},{"id":2278},"key-lessons",[2280],{"type":25,"value":2281},"Key Lessons",{"type":19,"tag":34,"props":2283,"children":2284},{},[2285,2295,2305,2315,2325,2335],{"type":19,"tag":38,"props":2286,"children":2287},{},[2288,2293],{"type":19,"tag":42,"props":2289,"children":2290},{},[2291],{"type":25,"value":2292},"Tokens are the foundation",{"type":25,"value":2294}," — invest heavily in design tokens before components",{"type":19,"tag":38,"props":2296,"children":2297},{},[2298,2303],{"type":19,"tag":42,"props":2299,"children":2300},{},[2301],{"type":25,"value":2302},"API design is everything",{"type":25,"value":2304}," — a good API is adopted naturally; a bad one requires enforcement",{"type":19,"tag":38,"props":2306,"children":2307},{},[2308,2313],{"type":19,"tag":42,"props":2309,"children":2310},{},[2311],{"type":25,"value":2312},"Storybook isn't optional",{"type":25,"value":2314}," — it's how designers and developers communicate",{"type":19,"tag":38,"props":2316,"children":2317},{},[2318,2323],{"type":19,"tag":42,"props":2319,"children":2320},{},[2321],{"type":25,"value":2322},"Version properly",{"type":25,"value":2324}," — teams need confidence that upgrades won't break things",{"type":19,"tag":38,"props":2326,"children":2327},{},[2328,2333],{"type":19,"tag":42,"props":2329,"children":2330},{},[2331],{"type":25,"value":2332},"One person owns it",{"type":25,"value":2334}," — design systems need a maintainer, not a committee",{"type":19,"tag":38,"props":2336,"children":2337},{},[2338,2343],{"type":19,"tag":42,"props":2339,"children":2340},{},[2341],{"type":25,"value":2342},"Measure success by adoption",{"type":25,"value":2344}," — a perfect system nobody uses is useless",{"type":19,"tag":28,"props":2346,"children":2347},{},[2348],{"type":25,"value":2349},"The design system went from idea to adoption in 4 months with 2 full-time engineers. The payoff: 30% faster feature development, 50% fewer component bugs, and consistent user experience across products.",{"title":7,"searchDepth":681,"depth":681,"links":2351},[2352,2353,2359,2360,2361,2362,2363],{"id":1833,"depth":681,"text":1836},{"id":1885,"depth":681,"text":1888,"children":2354},[2355,2356,2357,2358],{"id":1891,"depth":687,"text":1894},{"id":1938,"depth":687,"text":1941},{"id":2006,"depth":687,"text":2009},{"id":2054,"depth":687,"text":2057},{"id":2095,"depth":681,"text":2098},{"id":2115,"depth":681,"text":2118},{"id":2189,"depth":681,"text":2192},{"id":2260,"depth":681,"text":2263},{"id":2278,"depth":681,"text":2281},"content:blog:building-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue",{"_path":2368,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":2369,"description":2370,"date":2371,"updated":720,"readingTime":2372,"tags":2373,"featured":6,"body":2374,"_type":708,"_id":3169,"_source":710,"_file":3170,"_stem":3171,"_extension":713},"\u002Fblog\u002Ffrontend-architecture-for-large-saas","Frontend Architecture for Large SaaS: Monorepos, Micro-Frontends & Module Federation","Learn how to architect frontend systems for enterprise SaaS. Covers monorepo strategies, micro-frontend patterns, Module Federation, state management at scale, and team autonomy.","2024-07-15",15,[724,13],{"type":16,"children":2375,"toc":3147},[2376,2382,2387,2440,2445,2451,2456,2464,2470,2523,2529,2572,2577,2583,2588,2596,2601,2624,2630,2636,2645,2651,2660,2665,2688,2694,2699,2752,2758,2763,2772,2776,2819,2825,2830,2839,2845,2851,2859,2864,2873,2879,2888,2894,2899,2907,2913,2918,2926,2931,2937,2942,2993,2999,3004,3028,3033,3039,3044,3142],{"type":19,"tag":20,"props":2377,"children":2379},{"id":2378},"the-scaling-problem",[2380],{"type":25,"value":2381},"The Scaling Problem",{"type":19,"tag":28,"props":2383,"children":2384},{},[2385],{"type":25,"value":2386},"Our SaaS application started as a single Vue.js codebase. Three years later, 40 engineers across 8 teams were stepping on each other's toes:",{"type":19,"tag":50,"props":2388,"children":2389},{},[2390,2400,2410,2420,2430],{"type":19,"tag":38,"props":2391,"children":2392},{},[2393,2398],{"type":19,"tag":42,"props":2394,"children":2395},{},[2396],{"type":25,"value":2397},"Git conflicts",{"type":25,"value":2399}," — 20+ per day from different teams modifying the same files",{"type":19,"tag":38,"props":2401,"children":2402},{},[2403,2408],{"type":19,"tag":42,"props":2404,"children":2405},{},[2406],{"type":25,"value":2407},"Deployment bottlenecks",{"type":25,"value":2409}," — one bug in team A's feature blocked team B's release",{"type":19,"tag":38,"props":2411,"children":2412},{},[2413,2418],{"type":19,"tag":42,"props":2414,"children":2415},{},[2416],{"type":25,"value":2417},"Code ownership unclear",{"type":25,"value":2419}," — who owns the form validation layer? Three people claim they do",{"type":19,"tag":38,"props":2421,"children":2422},{},[2423,2428],{"type":19,"tag":42,"props":2424,"children":2425},{},[2426],{"type":25,"value":2427},"Bundle bloat",{"type":25,"value":2429}," — features for team A's customers were loading for team B's customers",{"type":19,"tag":38,"props":2431,"children":2432},{},[2433,2438],{"type":19,"tag":42,"props":2434,"children":2435},{},[2436],{"type":25,"value":2437},"Development friction",{"type":25,"value":2439}," — new team members took 4 weeks to understand the codebase",{"type":19,"tag":28,"props":2441,"children":2442},{},[2443],{"type":25,"value":2444},"Something had to change.",{"type":19,"tag":20,"props":2446,"children":2448},{"id":2447},"option-1-monorepo-what-we-chose",[2449],{"type":25,"value":2450},"Option 1: Monorepo (What We Chose)",{"type":19,"tag":28,"props":2452,"children":2453},{},[2454],{"type":25,"value":2455},"We chose a monorepo structure with pnpm workspaces:",{"type":19,"tag":128,"props":2457,"children":2459},{"code":2458},"apps\u002F\n├── dashboard\u002F          # Main SaaS dashboard\n├── admin-panel\u002F        # Admin-only features\n├── billing\u002F            # Standalone billing app\n└── docs\u002F               # Internal & external docs\n\npackages\u002F\n├── design-system\u002F      # Shared components\n├── api-client\u002F         # Shared API layer\n├── shared-utils\u002F       # Common utilities\n├── shared-state\u002F       # Pinia stores\n└── icons\u002F              # SVG icons library\n",[2460],{"type":19,"tag":133,"props":2461,"children":2462},{"__ignoreMap":7},[2463],{"type":25,"value":2458},{"type":19,"tag":121,"props":2465,"children":2467},{"id":2466},"monorepo-benefits",[2468],{"type":25,"value":2469},"Monorepo Benefits",{"type":19,"tag":50,"props":2471,"children":2472},{},[2473,2483,2493,2503,2513],{"type":19,"tag":38,"props":2474,"children":2475},{},[2476,2481],{"type":19,"tag":42,"props":2477,"children":2478},{},[2479],{"type":25,"value":2480},"Single source of truth",{"type":25,"value":2482}," for shared code",{"type":19,"tag":38,"props":2484,"children":2485},{},[2486,2491],{"type":19,"tag":42,"props":2487,"children":2488},{},[2489],{"type":25,"value":2490},"Atomic commits",{"type":25,"value":2492}," — can update multiple packages together",{"type":19,"tag":38,"props":2494,"children":2495},{},[2496,2501],{"type":19,"tag":42,"props":2497,"children":2498},{},[2499],{"type":25,"value":2500},"Shared CI\u002FCD",{"type":25,"value":2502}," — one build pipeline for all apps",{"type":19,"tag":38,"props":2504,"children":2505},{},[2506,2511],{"type":19,"tag":42,"props":2507,"children":2508},{},[2509],{"type":25,"value":2510},"Consistent tooling",{"type":25,"value":2512}," — eslint, prettier, typescript config",{"type":19,"tag":38,"props":2514,"children":2515},{},[2516,2521],{"type":19,"tag":42,"props":2517,"children":2518},{},[2519],{"type":25,"value":2520},"Easy refactoring",{"type":25,"value":2522}," — move code between apps without npm publish",{"type":19,"tag":121,"props":2524,"children":2526},{"id":2525},"monorepo-drawbacks",[2527],{"type":25,"value":2528},"Monorepo Drawbacks",{"type":19,"tag":50,"props":2530,"children":2531},{},[2532,2542,2552,2562],{"type":19,"tag":38,"props":2533,"children":2534},{},[2535,2540],{"type":19,"tag":42,"props":2536,"children":2537},{},[2538],{"type":25,"value":2539},"Build complexity",{"type":25,"value":2541}," — must rebuild only changed packages",{"type":19,"tag":38,"props":2543,"children":2544},{},[2545,2550],{"type":19,"tag":42,"props":2546,"children":2547},{},[2548],{"type":25,"value":2549},"Dependency management",{"type":25,"value":2551}," — circular dependencies possible",{"type":19,"tag":38,"props":2553,"children":2554},{},[2555,2560],{"type":19,"tag":42,"props":2556,"children":2557},{},[2558],{"type":25,"value":2559},"Harder to open-source",{"type":25,"value":2561}," — internal code in same repo",{"type":19,"tag":38,"props":2563,"children":2564},{},[2565,2570],{"type":19,"tag":42,"props":2566,"children":2567},{},[2568],{"type":25,"value":2569},"Steeper learning curve",{"type":25,"value":2571}," — new engineers need to understand workspace structure",{"type":19,"tag":28,"props":2573,"children":2574},{},[2575],{"type":25,"value":2576},"For us, benefits outweighed drawbacks.",{"type":19,"tag":20,"props":2578,"children":2580},{"id":2579},"option-2-micro-frontends-the-alternative",[2581],{"type":25,"value":2582},"Option 2: Micro-Frontends (The Alternative)",{"type":19,"tag":28,"props":2584,"children":2585},{},[2586],{"type":25,"value":2587},"For teams that want complete independence, micro-frontends are powerful:",{"type":19,"tag":128,"props":2589,"children":2591},{"code":2590},"main-app\u002F\n├── shell\u002F              # Shell\u002Fhost\n├── packages\u002F\n│   ├── dashboard-mfe\u002F  # Micro-frontend (team A)\n│   ├── billing-mfe\u002F    # Micro-frontend (team B)\n│   └── admin-mfe\u002F      # Micro-frontend (team C)\n",[2592],{"type":19,"tag":133,"props":2593,"children":2594},{"__ignoreMap":7},[2595],{"type":25,"value":2590},{"type":19,"tag":28,"props":2597,"children":2598},{},[2599],{"type":25,"value":2600},"Each micro-frontend:",{"type":19,"tag":50,"props":2602,"children":2603},{},[2604,2609,2614,2619],{"type":19,"tag":38,"props":2605,"children":2606},{},[2607],{"type":25,"value":2608},"Has its own git repo",{"type":19,"tag":38,"props":2610,"children":2611},{},[2612],{"type":25,"value":2613},"Deploys independently",{"type":19,"tag":38,"props":2615,"children":2616},{},[2617],{"type":25,"value":2618},"Ships its own bundle",{"type":19,"tag":38,"props":2620,"children":2621},{},[2622],{"type":25,"value":2623},"Uses shared design tokens + api client",{"type":19,"tag":121,"props":2625,"children":2627},{"id":2626},"micro-frontend-patterns",[2628],{"type":25,"value":2629},"Micro-Frontend Patterns",{"type":19,"tag":907,"props":2631,"children":2633},{"id":2632},"_1-module-federation-webpack-5-vite",[2634],{"type":25,"value":2635},"1. Module Federation (Webpack 5 \u002F Vite)",{"type":19,"tag":128,"props":2637,"children":2640},{"code":2638,"language":172,"meta":7,"className":2639},"\u002F\u002F host\u002Fvite.config.ts\nimport federation from '@originjs\u002Fvite-plugin-federation'\n\nexport default defineConfig({\n  plugins: [\n    federation({\n      name: 'host',\n      remotes: {\n        dashboard: 'http:\u002F\u002Flocalhost:5001\u002Fassets\u002FremoteEntry.js',\n        billing: 'http:\u002F\u002Flocalhost:5002\u002Fassets\u002FremoteEntry.js',\n        admin: 'http:\u002F\u002Flocalhost:5003\u002Fassets\u002FremoteEntry.js',\n      },\n      shared: ['vue', 'pinia', '@acme\u002Fapi-client']\n    })\n  ]\n})\n\n\u002F\u002F remote\u002Fvite.config.ts\nexport default defineConfig({\n  plugins: [\n    federation({\n      name: 'dashboard',\n      filename: 'remoteEntry.js',\n      exposes: {\n        '.\u002FApp': '.\u002Fsrc\u002FApp.vue',\n        '.\u002Fstore': '.\u002Fsrc\u002Fstore\u002Findex.ts'\n      },\n      shared: ['vue', 'pinia', '@acme\u002Fapi-client']\n    })\n  ]\n})\n",[174],[2641],{"type":19,"tag":133,"props":2642,"children":2643},{"__ignoreMap":7},[2644],{"type":25,"value":2638},{"type":19,"tag":907,"props":2646,"children":2648},{"id":2647},"_2-url-based-routing",[2649],{"type":25,"value":2650},"2. URL-based Routing",{"type":19,"tag":128,"props":2652,"children":2655},{"code":2653,"language":297,"meta":7,"className":2654},"\u003C!-- host\u002FApp.vue -->\n\u003Cscript setup>\nconst microFrontends = {\n  dashboard: { url: 'http:\u002F\u002Fdashboard.micro.local', path: '\u002Fdashboard' },\n  billing: { url: 'http:\u002F\u002Fbilling.micro.local', path: '\u002Fbilling' },\n  admin: { url: 'http:\u002F\u002Fadmin.micro.local', path: '\u002Fadmin' }\n}\n\nconst loadMicroFrontend = async (name) => {\n  const mfe = microFrontends[name]\n  const container = await import(mfe.url)\n  return container.App\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Crouter-view \u002F>\n\u003C\u002Ftemplate>\n",[299],[2656],{"type":19,"tag":133,"props":2657,"children":2658},{"__ignoreMap":7},[2659],{"type":25,"value":2653},{"type":19,"tag":28,"props":2661,"children":2662},{},[2663],{"type":25,"value":2664},"Each micro-frontend is a complete app that can:",{"type":19,"tag":50,"props":2666,"children":2667},{},[2668,2673,2678,2683],{"type":19,"tag":38,"props":2669,"children":2670},{},[2671],{"type":25,"value":2672},"Deploy independently",{"type":19,"tag":38,"props":2674,"children":2675},{},[2676],{"type":25,"value":2677},"Use different versions of dependencies (if needed)",{"type":19,"tag":38,"props":2679,"children":2680},{},[2681],{"type":25,"value":2682},"Be owned by a separate team",{"type":19,"tag":38,"props":2684,"children":2685},{},[2686],{"type":25,"value":2687},"Scale independently",{"type":19,"tag":20,"props":2689,"children":2691},{"id":2690},"our-monorepo-decision",[2692],{"type":25,"value":2693},"Our Monorepo Decision",{"type":19,"tag":28,"props":2695,"children":2696},{},[2697],{"type":25,"value":2698},"We chose monorepo because:",{"type":19,"tag":34,"props":2700,"children":2701},{},[2702,2712,2722,2732,2742],{"type":19,"tag":38,"props":2703,"children":2704},{},[2705,2710],{"type":19,"tag":42,"props":2706,"children":2707},{},[2708],{"type":25,"value":2709},"Team maturity",{"type":25,"value":2711}," — all teams understood git and npm workspaces",{"type":19,"tag":38,"props":2713,"children":2714},{},[2715,2720],{"type":19,"tag":42,"props":2716,"children":2717},{},[2718],{"type":25,"value":2719},"Shared design system",{"type":25,"value":2721}," — critical for consistent UX",{"type":19,"tag":38,"props":2723,"children":2724},{},[2725,2730],{"type":19,"tag":42,"props":2726,"children":2727},{},[2728],{"type":25,"value":2729},"Single deployment",{"type":25,"value":2731}," — easier for our ops team",{"type":19,"tag":38,"props":2733,"children":2734},{},[2735,2740],{"type":19,"tag":42,"props":2736,"children":2737},{},[2738],{"type":25,"value":2739},"Developer experience",{"type":25,"value":2741}," — IDE support better in monorepo",{"type":19,"tag":38,"props":2743,"children":2744},{},[2745,2750],{"type":19,"tag":42,"props":2746,"children":2747},{},[2748],{"type":25,"value":2749},"Build optimization",{"type":25,"value":2751}," — turborepo handles incremental builds well",{"type":19,"tag":20,"props":2753,"children":2755},{"id":2754},"implementation-turborepo",[2756],{"type":25,"value":2757},"Implementation: Turborepo",{"type":19,"tag":28,"props":2759,"children":2760},{},[2761],{"type":25,"value":2762},"We use Turborepo for build orchestration:",{"type":19,"tag":128,"props":2764,"children":2767},{"code":2765,"language":2179,"meta":7,"className":2766},"{\n  \"$schema\": \"https:\u002F\u002Fturbo.build\u002Fjson-schema.json\",\n  \"pipeline\": {\n    \"build\": {\n      \"dependsOn\": [\"^build\"],\n      \"outputs\": [\"dist\u002F**\"],\n      \"cache\": true\n    },\n    \"test\": {\n      \"dependsOn\": [\"build\"],\n      \"outputs\": [],\n      \"cache\": true\n    },\n    \"lint\": {\n      \"outputs\": []\n    },\n    \"dev\": {\n      \"cache\": false,\n      \"persistent\": true\n    }\n  },\n  \"globalEnv\": [\"NODE_ENV\", \"VERCEL_ENV\"],\n  \"globalDependencies\": [\"tsconfig.json\", \".eslintrc.js\"]\n}\n",[2181],[2768],{"type":19,"tag":133,"props":2769,"children":2770},{"__ignoreMap":7},[2771],{"type":25,"value":2765},{"type":19,"tag":28,"props":2773,"children":2774},{},[2775],{"type":25,"value":1912},{"type":19,"tag":50,"props":2777,"children":2778},{},[2779,2789,2799,2809],{"type":19,"tag":38,"props":2780,"children":2781},{},[2782,2787],{"type":19,"tag":42,"props":2783,"children":2784},{},[2785],{"type":25,"value":2786},"Parallel execution",{"type":25,"value":2788}," — builds run in parallel when possible",{"type":19,"tag":38,"props":2790,"children":2791},{},[2792,2797],{"type":19,"tag":42,"props":2793,"children":2794},{},[2795],{"type":25,"value":2796},"Task pipelines",{"type":25,"value":2798}," — test only runs after build",{"type":19,"tag":38,"props":2800,"children":2801},{},[2802,2807],{"type":19,"tag":42,"props":2803,"children":2804},{},[2805],{"type":25,"value":2806},"Caching",{"type":25,"value":2808}," — skips tasks that haven't changed",{"type":19,"tag":38,"props":2810,"children":2811},{},[2812,2817],{"type":19,"tag":42,"props":2813,"children":2814},{},[2815],{"type":25,"value":2816},"CI\u002FCD integration",{"type":25,"value":2818}," — auto-detects changed packages",{"type":19,"tag":20,"props":2820,"children":2822},{"id":2821},"state-management-at-scale",[2823],{"type":25,"value":2824},"State Management at Scale",{"type":19,"tag":28,"props":2826,"children":2827},{},[2828],{"type":25,"value":2829},"Pinia (Vue's official state management) handles monorepo scales well:",{"type":19,"tag":128,"props":2831,"children":2834},{"code":2832,"language":172,"meta":7,"className":2833},"\u002F\u002F packages\u002Fshared-state\u002Fsrc\u002Fstores\u002Fauth.ts\nimport { defineStore } from 'pinia'\n\nexport const useAuthStore = defineStore('auth', () => {\n  const user = ref(null)\n  const isAuthenticated = computed(() => !!user.value)\n  \n  const login = async (credentials) => {\n    const response = await api.login(credentials)\n    user.value = response.user\n    localStorage.setItem('token', response.token)\n  }\n  \n  const logout = () => {\n    user.value = null\n    localStorage.removeItem('token')\n  }\n  \n  return { user, isAuthenticated, login, logout }\n})\n\n\u002F\u002F Usage in any app\nimport { useAuthStore } from '@acme\u002Fshared-state'\n\nconst authStore = useAuthStore()\nauthStore.login(credentials)\n",[174],[2835],{"type":19,"tag":133,"props":2836,"children":2837},{"__ignoreMap":7},[2838],{"type":25,"value":2832},{"type":19,"tag":20,"props":2840,"children":2842},{"id":2841},"preventing-common-pitfalls",[2843],{"type":25,"value":2844},"Preventing Common Pitfalls",{"type":19,"tag":121,"props":2846,"children":2848},{"id":2847},"_1-circular-dependencies",[2849],{"type":25,"value":2850},"1. Circular Dependencies",{"type":19,"tag":128,"props":2852,"children":2854},{"code":2853},"Bad: dashboard → api-client → shared-utils → dashboard\n",[2855],{"type":19,"tag":133,"props":2856,"children":2857},{"__ignoreMap":7},[2858],{"type":25,"value":2853},{"type":19,"tag":28,"props":2860,"children":2861},{},[2862],{"type":25,"value":2863},"Solution: Enforce dependency graph with eslint:",{"type":19,"tag":128,"props":2865,"children":2868},{"code":2866,"language":1006,"meta":7,"className":2867},"\u002F\u002F .eslintrc.js\n{\n  \"rules\": {\n    \"import\u002Fno-cycle\": \"error\"\n  }\n}\n",[1004],[2869],{"type":19,"tag":133,"props":2870,"children":2871},{"__ignoreMap":7},[2872],{"type":25,"value":2866},{"type":19,"tag":121,"props":2874,"children":2876},{"id":2875},"_2-shared-dependency-versions",[2877],{"type":25,"value":2878},"2. Shared Dependency Versions",{"type":19,"tag":128,"props":2880,"children":2883},{"code":2881,"language":172,"meta":7,"className":2882},"\u002F\u002F Root tsconfig.json\n{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\",\n    \"resolvePackageJsonExports\": true\n  },\n  \"extends\": \".\u002Ftsconfig.base.json\"\n}\n\n\u002F\u002F pnpm-workspace.yaml\npackages:\n  - 'packages\u002F*'\n  - 'apps\u002F*'\n\n# Lockfile ensures all packages use same versions\n",[174],[2884],{"type":19,"tag":133,"props":2885,"children":2886},{"__ignoreMap":7},[2887],{"type":25,"value":2881},{"type":19,"tag":121,"props":2889,"children":2891},{"id":2890},"_3-code-ownership",[2892],{"type":25,"value":2893},"3. Code Ownership",{"type":19,"tag":28,"props":2895,"children":2896},{},[2897],{"type":25,"value":2898},"Create a CODEOWNERS file:",{"type":19,"tag":128,"props":2900,"children":2902},{"code":2901},"# CODEOWNERS\npackages\u002Fdesign-system\u002F  @design-team\npackages\u002Fapi-client\u002F     @backend-team, @frontend-team\napps\u002Fdashboard\u002F          @dashboard-team\napps\u002Fadmin\u002F              @admin-team\n",[2903],{"type":19,"tag":133,"props":2904,"children":2905},{"__ignoreMap":7},[2906],{"type":25,"value":2901},{"type":19,"tag":20,"props":2908,"children":2910},{"id":2909},"performance-impact",[2911],{"type":25,"value":2912},"Performance Impact",{"type":19,"tag":28,"props":2914,"children":2915},{},[2916],{"type":25,"value":2917},"Monorepo with proper caching:",{"type":19,"tag":128,"props":2919,"children":2921},{"code":2920},"Before: npm run build (all apps)\nReal:  45 seconds\nUser:  78 seconds (with npm overhead)\n\nAfter: turbo build (only changed apps)\nReal:  12 seconds (cached)\nUser:  3 seconds (with caching)\n",[2922],{"type":19,"tag":133,"props":2923,"children":2924},{"__ignoreMap":7},[2925],{"type":25,"value":2920},{"type":19,"tag":28,"props":2927,"children":2928},{},[2929],{"type":25,"value":2930},"Cached builds like this reclaim real hours of CI time every week — measure your own before\u002Fafter; the delta is usually the easiest infrastructure win of the quarter.",{"type":19,"tag":20,"props":2932,"children":2934},{"id":2933},"team-autonomy-in-monorepo",[2935],{"type":25,"value":2936},"Team Autonomy in Monorepo",{"type":19,"tag":28,"props":2938,"children":2939},{},[2940],{"type":25,"value":2941},"To prevent chaos:",{"type":19,"tag":34,"props":2943,"children":2944},{},[2945,2955,2965,2983],{"type":19,"tag":38,"props":2946,"children":2947},{},[2948,2953],{"type":19,"tag":42,"props":2949,"children":2950},{},[2951],{"type":25,"value":2952},"Clear package boundaries",{"type":25,"value":2954}," — dashboard team doesn't touch billing code",{"type":19,"tag":38,"props":2956,"children":2957},{},[2958,2963],{"type":19,"tag":42,"props":2959,"children":2960},{},[2961],{"type":25,"value":2962},"Owned packages",{"type":25,"value":2964}," — each package has a code owner",{"type":19,"tag":38,"props":2966,"children":2967},{},[2968,2973,2975,2981],{"type":19,"tag":42,"props":2969,"children":2970},{},[2971],{"type":25,"value":2972},"Version contracts",{"type":25,"value":2974}," — ",{"type":19,"tag":133,"props":2976,"children":2978},{"className":2977},[],[2979],{"type":25,"value":2980},"@acme\u002Fapi-client",{"type":25,"value":2982}," v2.0.0 is stable, don't break it",{"type":19,"tag":38,"props":2984,"children":2985},{},[2986,2991],{"type":19,"tag":42,"props":2987,"children":2988},{},[2989],{"type":25,"value":2990},"Shared conventions",{"type":25,"value":2992}," — naming, folder structure, testing patterns",{"type":19,"tag":20,"props":2994,"children":2996},{"id":2995},"when-monorepo-isnt-enough",[2997],{"type":25,"value":2998},"When Monorepo Isn't Enough",{"type":19,"tag":28,"props":3000,"children":3001},{},[3002],{"type":25,"value":3003},"For teams that need complete independence, consider micro-frontends:",{"type":19,"tag":28,"props":3005,"children":3006},{},[3007,3012,3014,3019,3021,3026],{"type":19,"tag":42,"props":3008,"children":3009},{},[3010],{"type":25,"value":3011},"Monorepo",{"type":25,"value":3013}," → Single deployment, shared everything\n",{"type":19,"tag":42,"props":3015,"children":3016},{},[3017],{"type":25,"value":3018},"Micro-Frontends",{"type":25,"value":3020}," → Independent deployments, isolated teams\n",{"type":19,"tag":42,"props":3022,"children":3023},{},[3024],{"type":25,"value":3025},"Hybrid",{"type":25,"value":3027}," → Monorepo for shared packages, micro-frontends for apps",{"type":19,"tag":28,"props":3029,"children":3030},{},[3031],{"type":25,"value":3032},"We started with monorepo. If we grew to 200 engineers across 30 teams, we'd likely move to micro-frontends with Module Federation.",{"type":19,"tag":20,"props":3034,"children":3036},{"id":3035},"final-metrics",[3037],{"type":25,"value":3038},"Final Metrics",{"type":19,"tag":28,"props":3040,"children":3041},{},[3042],{"type":25,"value":3043},"The movement a monorepo migration typically produces (illustrative example — not measured claims from one project):",{"type":19,"tag":478,"props":3045,"children":3046},{},[3047,3062],{"type":19,"tag":482,"props":3048,"children":3049},{},[3050],{"type":19,"tag":486,"props":3051,"children":3052},{},[3053,3057],{"type":19,"tag":490,"props":3054,"children":3055},{},[3056],{"type":25,"value":494},{"type":19,"tag":490,"props":3058,"children":3059},{},[3060],{"type":25,"value":3061},"Direction",{"type":19,"tag":506,"props":3063,"children":3064},{},[3065,3078,3091,3103,3116,3129],{"type":19,"tag":486,"props":3066,"children":3067},{},[3068,3073],{"type":19,"tag":513,"props":3069,"children":3070},{},[3071],{"type":25,"value":3072},"Build time",{"type":19,"tag":513,"props":3074,"children":3075},{},[3076],{"type":25,"value":3077},"Sharply down with caching",{"type":19,"tag":486,"props":3079,"children":3080},{},[3081,3086],{"type":19,"tag":513,"props":3082,"children":3083},{},[3084],{"type":25,"value":3085},"CI\u002FCD time",{"type":19,"tag":513,"props":3087,"children":3088},{},[3089],{"type":25,"value":3090},"Down, especially on unchanged packages",{"type":19,"tag":486,"props":3092,"children":3093},{},[3094,3098],{"type":19,"tag":513,"props":3095,"children":3096},{},[3097],{"type":25,"value":2397},{"type":19,"tag":513,"props":3099,"children":3100},{},[3101],{"type":25,"value":3102},"Down — one repo, one integration point",{"type":19,"tag":486,"props":3104,"children":3105},{},[3106,3111],{"type":19,"tag":513,"props":3107,"children":3108},{},[3109],{"type":25,"value":3110},"Deployment failures",{"type":19,"tag":513,"props":3112,"children":3113},{},[3114],{"type":25,"value":3115},"Down — shared tooling, fewer bespoke pipelines",{"type":19,"tag":486,"props":3117,"children":3118},{},[3119,3124],{"type":19,"tag":513,"props":3120,"children":3121},{},[3122],{"type":25,"value":3123},"Code duplication",{"type":19,"tag":513,"props":3125,"children":3126},{},[3127],{"type":25,"value":3128},"Down — importing beats re-implementing",{"type":19,"tag":486,"props":3130,"children":3131},{},[3132,3137],{"type":19,"tag":513,"props":3133,"children":3134},{},[3135],{"type":25,"value":3136},"Onboarding time",{"type":19,"tag":513,"props":3138,"children":3139},{},[3140],{"type":25,"value":3141},"Down — one setup, one set of conventions",{"type":19,"tag":28,"props":3143,"children":3144},{},[3145],{"type":25,"value":3146},"The architecture change pays off through clear ownership, faster development, and fewer bugs — if you enforce the boundaries.",{"title":7,"searchDepth":681,"depth":681,"links":3148},[3149,3150,3154,3157,3158,3159,3160,3165,3166,3167,3168],{"id":2378,"depth":681,"text":2381},{"id":2447,"depth":681,"text":2450,"children":3151},[3152,3153],{"id":2466,"depth":687,"text":2469},{"id":2525,"depth":687,"text":2528},{"id":2579,"depth":681,"text":2582,"children":3155},[3156],{"id":2626,"depth":687,"text":2629},{"id":2690,"depth":681,"text":2693},{"id":2754,"depth":681,"text":2757},{"id":2821,"depth":681,"text":2824},{"id":2841,"depth":681,"text":2844,"children":3161},[3162,3163,3164],{"id":2847,"depth":687,"text":2850},{"id":2875,"depth":687,"text":2878},{"id":2890,"depth":687,"text":2893},{"id":2909,"depth":681,"text":2912},{"id":2933,"depth":681,"text":2936},{"id":2995,"depth":681,"text":2998},{"id":3035,"depth":681,"text":3038},"content:blog:frontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"readingTime":11,"tags":3173,"featured":6,"body":3174,"_type":708,"_id":709,"_source":710,"_file":711,"_stem":712,"_extension":713},[13,14],{"type":16,"children":3175,"toc":3693},[3176,3180,3184,3248,3252,3256,3263,3267,3271,3275,3283,3287,3295,3299,3307,3311,3315,3319,3323,3331,3335,3350,3354,3362,3366,3370,3378,3382,3386,3394,3398,3402,3406,3414,3418,3426,3430,3438,3442,3446,3486,3490,3498,3502,3510,3514,3522,3526,3530,3614,3618,3622,3630,3634,3642,3646,3689],{"type":19,"tag":20,"props":3177,"children":3178},{"id":22},[3179],{"type":25,"value":26},{"type":19,"tag":28,"props":3181,"children":3182},{},[3183],{"type":25,"value":32},{"type":19,"tag":34,"props":3185,"children":3186},{},[3187,3206,3229],{"type":19,"tag":38,"props":3188,"children":3189},{},[3190,3194,3195],{"type":19,"tag":42,"props":3191,"children":3192},{},[3193],{"type":25,"value":46},{"type":25,"value":48},{"type":19,"tag":50,"props":3196,"children":3197},{},[3198,3202],{"type":19,"tag":38,"props":3199,"children":3200},{},[3201],{"type":25,"value":57},{"type":19,"tag":38,"props":3203,"children":3204},{},[3205],{"type":25,"value":62},{"type":19,"tag":38,"props":3207,"children":3208},{},[3209,3213,3214],{"type":19,"tag":42,"props":3210,"children":3211},{},[3212],{"type":25,"value":70},{"type":25,"value":72},{"type":19,"tag":50,"props":3215,"children":3216},{},[3217,3221,3225],{"type":19,"tag":38,"props":3218,"children":3219},{},[3220],{"type":25,"value":80},{"type":19,"tag":38,"props":3222,"children":3223},{},[3224],{"type":25,"value":85},{"type":19,"tag":38,"props":3226,"children":3227},{},[3228],{"type":25,"value":90},{"type":19,"tag":38,"props":3230,"children":3231},{},[3232,3236,3237],{"type":19,"tag":42,"props":3233,"children":3234},{},[3235],{"type":25,"value":98},{"type":25,"value":100},{"type":19,"tag":50,"props":3238,"children":3239},{},[3240,3244],{"type":19,"tag":38,"props":3241,"children":3242},{},[3243],{"type":25,"value":108},{"type":19,"tag":38,"props":3245,"children":3246},{},[3247],{"type":25,"value":113},{"type":19,"tag":20,"props":3249,"children":3250},{"id":116},[3251],{"type":25,"value":119},{"type":19,"tag":121,"props":3253,"children":3254},{"id":123},[3255],{"type":25,"value":126},{"type":19,"tag":128,"props":3257,"children":3258},{"code":130},[3259],{"type":19,"tag":133,"props":3260,"children":3261},{"__ignoreMap":7},[3262],{"type":25,"value":130},{"type":19,"tag":28,"props":3264,"children":3265},{},[3266],{"type":25,"value":141},{"type":19,"tag":121,"props":3268,"children":3269},{"id":144},[3270],{"type":25,"value":147},{"type":19,"tag":28,"props":3272,"children":3273},{},[3274],{"type":25,"value":152},{"type":19,"tag":128,"props":3276,"children":3278},{"code":155,"language":156,"meta":7,"className":3277},[158],[3279],{"type":19,"tag":133,"props":3280,"children":3281},{"__ignoreMap":7},[3282],{"type":25,"value":155},{"type":19,"tag":28,"props":3284,"children":3285},{},[3286],{"type":25,"value":168},{"type":19,"tag":128,"props":3288,"children":3290},{"code":171,"language":172,"meta":7,"className":3289},[174],[3291],{"type":19,"tag":133,"props":3292,"children":3293},{"__ignoreMap":7},[3294],{"type":25,"value":171},{"type":19,"tag":28,"props":3296,"children":3297},{},[3298],{"type":25,"value":184},{"type":19,"tag":128,"props":3300,"children":3302},{"code":187,"language":172,"meta":7,"className":3301},[174],[3303],{"type":19,"tag":133,"props":3304,"children":3305},{"__ignoreMap":7},[3306],{"type":25,"value":187},{"type":19,"tag":20,"props":3308,"children":3309},{"id":196},[3310],{"type":25,"value":199},{"type":19,"tag":28,"props":3312,"children":3313},{},[3314],{"type":25,"value":204},{"type":19,"tag":121,"props":3316,"children":3317},{"id":207},[3318],{"type":25,"value":210},{"type":19,"tag":28,"props":3320,"children":3321},{},[3322],{"type":25,"value":215},{"type":19,"tag":128,"props":3324,"children":3326},{"code":218,"language":172,"meta":7,"className":3325},[174],[3327],{"type":19,"tag":133,"props":3328,"children":3329},{"__ignoreMap":7},[3330],{"type":25,"value":218},{"type":19,"tag":28,"props":3332,"children":3333},{},[3334],{"type":25,"value":229},{"type":19,"tag":50,"props":3336,"children":3337},{},[3338,3342,3346],{"type":19,"tag":38,"props":3339,"children":3340},{},[3341],{"type":25,"value":237},{"type":19,"tag":38,"props":3343,"children":3344},{},[3345],{"type":25,"value":242},{"type":19,"tag":38,"props":3347,"children":3348},{},[3349],{"type":25,"value":247},{"type":19,"tag":121,"props":3351,"children":3352},{"id":250},[3353],{"type":25,"value":253},{"type":19,"tag":128,"props":3355,"children":3357},{"code":256,"language":172,"meta":7,"className":3356},[174],[3358],{"type":19,"tag":133,"props":3359,"children":3360},{"__ignoreMap":7},[3361],{"type":25,"value":256},{"type":19,"tag":121,"props":3363,"children":3364},{"id":265},[3365],{"type":25,"value":268},{"type":19,"tag":28,"props":3367,"children":3368},{},[3369],{"type":25,"value":273},{"type":19,"tag":128,"props":3371,"children":3373},{"code":276,"language":172,"meta":7,"className":3372},[174],[3374],{"type":19,"tag":133,"props":3375,"children":3376},{"__ignoreMap":7},[3377],{"type":25,"value":276},{"type":19,"tag":121,"props":3379,"children":3380},{"id":285},[3381],{"type":25,"value":288},{"type":19,"tag":28,"props":3383,"children":3384},{},[3385],{"type":25,"value":293},{"type":19,"tag":128,"props":3387,"children":3389},{"code":296,"language":297,"meta":7,"className":3388},[299],[3390],{"type":19,"tag":133,"props":3391,"children":3392},{"__ignoreMap":7},[3393],{"type":25,"value":296},{"type":19,"tag":20,"props":3395,"children":3396},{"id":307},[3397],{"type":25,"value":310},{"type":19,"tag":28,"props":3399,"children":3400},{},[3401],{"type":25,"value":315},{"type":19,"tag":121,"props":3403,"children":3404},{"id":318},[3405],{"type":25,"value":321},{"type":19,"tag":128,"props":3407,"children":3409},{"code":324,"language":172,"meta":7,"className":3408},[174],[3410],{"type":19,"tag":133,"props":3411,"children":3412},{"__ignoreMap":7},[3413],{"type":25,"value":324},{"type":19,"tag":121,"props":3415,"children":3416},{"id":333},[3417],{"type":25,"value":336},{"type":19,"tag":128,"props":3419,"children":3421},{"code":339,"language":172,"meta":7,"className":3420},[174],[3422],{"type":19,"tag":133,"props":3423,"children":3424},{"__ignoreMap":7},[3425],{"type":25,"value":339},{"type":19,"tag":121,"props":3427,"children":3428},{"id":348},[3429],{"type":25,"value":351},{"type":19,"tag":128,"props":3431,"children":3433},{"code":354,"language":297,"meta":7,"className":3432},[299],[3434],{"type":19,"tag":133,"props":3435,"children":3436},{"__ignoreMap":7},[3437],{"type":25,"value":354},{"type":19,"tag":20,"props":3439,"children":3440},{"id":363},[3441],{"type":25,"value":366},{"type":19,"tag":28,"props":3443,"children":3444},{},[3445],{"type":25,"value":371},{"type":19,"tag":34,"props":3447,"children":3448},{},[3449,3457,3465,3478],{"type":19,"tag":38,"props":3450,"children":3451},{},[3452,3456],{"type":19,"tag":42,"props":3453,"children":3454},{},[3455],{"type":25,"value":382},{"type":25,"value":384},{"type":19,"tag":38,"props":3458,"children":3459},{},[3460,3464],{"type":19,"tag":42,"props":3461,"children":3462},{},[3463],{"type":25,"value":392},{"type":25,"value":394},{"type":19,"tag":38,"props":3466,"children":3467},{},[3468,3472,3473],{"type":19,"tag":42,"props":3469,"children":3470},{},[3471],{"type":25,"value":402},{"type":25,"value":404},{"type":19,"tag":133,"props":3474,"children":3476},{"className":3475},[],[3477],{"type":25,"value":410},{"type":19,"tag":38,"props":3479,"children":3480},{},[3481,3485],{"type":19,"tag":42,"props":3482,"children":3483},{},[3484],{"type":25,"value":418},{"type":25,"value":420},{"type":19,"tag":121,"props":3487,"children":3488},{"id":423},[3489],{"type":25,"value":426},{"type":19,"tag":128,"props":3491,"children":3493},{"code":429,"language":297,"meta":7,"className":3492},[299],[3494],{"type":19,"tag":133,"props":3495,"children":3496},{"__ignoreMap":7},[3497],{"type":25,"value":429},{"type":19,"tag":121,"props":3499,"children":3500},{"id":438},[3501],{"type":25,"value":441},{"type":19,"tag":128,"props":3503,"children":3505},{"code":444,"language":172,"meta":7,"className":3504},[174],[3506],{"type":19,"tag":133,"props":3507,"children":3508},{"__ignoreMap":7},[3509],{"type":25,"value":444},{"type":19,"tag":121,"props":3511,"children":3512},{"id":453},[3513],{"type":25,"value":456},{"type":19,"tag":128,"props":3515,"children":3517},{"code":459,"language":297,"meta":7,"className":3516},[299],[3518],{"type":19,"tag":133,"props":3519,"children":3520},{"__ignoreMap":7},[3521],{"type":25,"value":459},{"type":19,"tag":20,"props":3523,"children":3524},{"id":468},[3525],{"type":25,"value":471},{"type":19,"tag":28,"props":3527,"children":3528},{},[3529],{"type":25,"value":476},{"type":19,"tag":478,"props":3531,"children":3532},{},[3533,3551],{"type":19,"tag":482,"props":3534,"children":3535},{},[3536],{"type":19,"tag":486,"props":3537,"children":3538},{},[3539,3543,3547],{"type":19,"tag":490,"props":3540,"children":3541},{},[3542],{"type":25,"value":494},{"type":19,"tag":490,"props":3544,"children":3545},{},[3546],{"type":25,"value":499},{"type":19,"tag":490,"props":3548,"children":3549},{},[3550],{"type":25,"value":504},{"type":19,"tag":506,"props":3552,"children":3553},{},[3554,3569,3584,3599],{"type":19,"tag":486,"props":3555,"children":3556},{},[3557,3561,3565],{"type":19,"tag":513,"props":3558,"children":3559},{},[3560],{"type":25,"value":517},{"type":19,"tag":513,"props":3562,"children":3563},{},[3564],{"type":25,"value":522},{"type":19,"tag":513,"props":3566,"children":3567},{},[3568],{"type":25,"value":527},{"type":19,"tag":486,"props":3570,"children":3571},{},[3572,3576,3580],{"type":19,"tag":513,"props":3573,"children":3574},{},[3575],{"type":25,"value":535},{"type":19,"tag":513,"props":3577,"children":3578},{},[3579],{"type":25,"value":540},{"type":19,"tag":513,"props":3581,"children":3582},{},[3583],{"type":25,"value":545},{"type":19,"tag":486,"props":3585,"children":3586},{},[3587,3591,3595],{"type":19,"tag":513,"props":3588,"children":3589},{},[3590],{"type":25,"value":553},{"type":19,"tag":513,"props":3592,"children":3593},{},[3594],{"type":25,"value":558},{"type":19,"tag":513,"props":3596,"children":3597},{},[3598],{"type":25,"value":563},{"type":19,"tag":486,"props":3600,"children":3601},{},[3602,3606,3610],{"type":19,"tag":513,"props":3603,"children":3604},{},[3605],{"type":25,"value":571},{"type":19,"tag":513,"props":3607,"children":3608},{},[3609],{"type":25,"value":576},{"type":19,"tag":513,"props":3611,"children":3612},{},[3613],{"type":25,"value":581},{"type":19,"tag":20,"props":3615,"children":3616},{"id":584},[3617],{"type":25,"value":587},{"type":19,"tag":28,"props":3619,"children":3620},{},[3621],{"type":25,"value":592},{"type":19,"tag":128,"props":3623,"children":3625},{"code":595,"language":172,"meta":7,"className":3624},[174],[3626],{"type":19,"tag":133,"props":3627,"children":3628},{"__ignoreMap":7},[3629],{"type":25,"value":595},{"type":19,"tag":28,"props":3631,"children":3632},{},[3633],{"type":25,"value":606},{"type":19,"tag":128,"props":3635,"children":3637},{"code":609,"language":156,"meta":7,"className":3636},[158],[3638],{"type":19,"tag":133,"props":3639,"children":3640},{"__ignoreMap":7},[3641],{"type":25,"value":609},{"type":19,"tag":20,"props":3643,"children":3644},{"id":618},[3645],{"type":25,"value":621},{"type":19,"tag":34,"props":3647,"children":3648},{},[3649,3657,3665,3673,3681],{"type":19,"tag":38,"props":3650,"children":3651},{},[3652,3656],{"type":19,"tag":42,"props":3653,"children":3654},{},[3655],{"type":25,"value":632},{"type":25,"value":634},{"type":19,"tag":38,"props":3658,"children":3659},{},[3660,3664],{"type":19,"tag":42,"props":3661,"children":3662},{},[3663],{"type":25,"value":642},{"type":25,"value":644},{"type":19,"tag":38,"props":3666,"children":3667},{},[3668,3672],{"type":19,"tag":42,"props":3669,"children":3670},{},[3671],{"type":25,"value":652},{"type":25,"value":654},{"type":19,"tag":38,"props":3674,"children":3675},{},[3676,3680],{"type":19,"tag":42,"props":3677,"children":3678},{},[3679],{"type":25,"value":662},{"type":25,"value":664},{"type":19,"tag":38,"props":3682,"children":3683},{},[3684,3688],{"type":19,"tag":42,"props":3685,"children":3686},{},[3687],{"type":25,"value":672},{"type":25,"value":674},{"type":19,"tag":28,"props":3690,"children":3691},{},[3692],{"type":25,"value":679},{"title":7,"searchDepth":681,"depth":681,"links":3694},[3695,3696,3700,3706,3711,3716,3717,3718],{"id":22,"depth":681,"text":26},{"id":116,"depth":681,"text":119,"children":3697},[3698,3699],{"id":123,"depth":687,"text":126},{"id":144,"depth":687,"text":147},{"id":196,"depth":681,"text":199,"children":3701},[3702,3703,3704,3705],{"id":207,"depth":687,"text":210},{"id":250,"depth":687,"text":253},{"id":265,"depth":687,"text":268},{"id":285,"depth":687,"text":288},{"id":307,"depth":681,"text":310,"children":3707},[3708,3709,3710],{"id":318,"depth":687,"text":321},{"id":333,"depth":687,"text":336},{"id":348,"depth":687,"text":351},{"id":363,"depth":681,"text":366,"children":3712},[3713,3714,3715],{"id":423,"depth":687,"text":426},{"id":438,"depth":687,"text":441},{"id":453,"depth":687,"text":456},{"id":468,"depth":681,"text":471},{"id":584,"depth":681,"text":587},{"id":618,"depth":681,"text":621},{"_path":3720,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":3721,"description":3722,"date":3723,"updated":720,"readingTime":721,"tags":3724,"featured":6,"body":3727,"_type":708,"_id":4147,"_source":710,"_file":4148,"_stem":4149,"_extension":713},"\u002Fblog\u002Fadvanced-frontend-ci-cd-workflows","Advanced Frontend CI\u002FCD Workflows: From Code to Production in 5 Minutes","Build automated CI\u002FCD pipelines for frontend apps. Learn GitHub Actions, parallel testing, performance monitoring, semantic versioning, and production deployments.","2024-06-10",[3725,3726],"CI\u002FCD","DevOps",{"type":16,"children":3728,"toc":4131},[3729,3735,3740,3745,3758,3763,3768,3821,3827,3835,3841,3852,3858,3864,3869,3878,3883,3889,3898,3911,3917,3926,3931,3937,3946,3951,3979,3984,3990,3999,4004,4010,4019,4025,4030,4041,4047,4052,4057,4062,4067,4073,4126],{"type":19,"tag":20,"props":3730,"children":3732},{"id":3731},"the-state-of-frontend-cicd",[3733],{"type":25,"value":3734},"The State of Frontend CI\u002FCD",{"type":19,"tag":28,"props":3736,"children":3737},{},[3738],{"type":25,"value":3739},"The \"5 minutes\" in the title is the target this guide builds toward — a design goal for the pipeline, not a production stat I'm claiming. My own verified result is more modest and stated at the end.",{"type":19,"tag":28,"props":3741,"children":3742},{},[3743],{"type":25,"value":3744},"Many teams still use basic CI pipelines:",{"type":19,"tag":34,"props":3746,"children":3747},{},[3748,3753],{"type":19,"tag":38,"props":3749,"children":3750},{},[3751],{"type":25,"value":3752},"Run tests",{"type":19,"tag":38,"props":3754,"children":3755},{},[3756],{"type":25,"value":3757},"If pass, deploy",{"type":19,"tag":28,"props":3759,"children":3760},{},[3761],{"type":25,"value":3762},"Result: slow feedback, missed bugs, brittle deployments.",{"type":19,"tag":28,"props":3764,"children":3765},{},[3766],{"type":25,"value":3767},"Modern CI\u002FCD is sophisticated:",{"type":19,"tag":34,"props":3769,"children":3770},{},[3771,3781,3791,3801,3811],{"type":19,"tag":38,"props":3772,"children":3773},{},[3774,3779],{"type":19,"tag":42,"props":3775,"children":3776},{},[3777],{"type":25,"value":3778},"Parallel testing",{"type":25,"value":3780}," — unit, integration, e2e at the same time",{"type":19,"tag":38,"props":3782,"children":3783},{},[3784,3789],{"type":19,"tag":42,"props":3785,"children":3786},{},[3787],{"type":25,"value":3788},"Performance monitoring",{"type":25,"value":3790}," — reject PRs that slow down the app",{"type":19,"tag":38,"props":3792,"children":3793},{},[3794,3799],{"type":19,"tag":42,"props":3795,"children":3796},{},[3797],{"type":25,"value":3798},"Visual regression testing",{"type":25,"value":3800}," — catch unintended design changes",{"type":19,"tag":38,"props":3802,"children":3803},{},[3804,3809],{"type":19,"tag":42,"props":3805,"children":3806},{},[3807],{"type":25,"value":3808},"Semantic versioning",{"type":25,"value":3810}," — automate version bumps and releases",{"type":19,"tag":38,"props":3812,"children":3813},{},[3814,3819],{"type":19,"tag":42,"props":3815,"children":3816},{},[3817],{"type":25,"value":3818},"Multi-stage deployments",{"type":25,"value":3820}," — preview → staging → production",{"type":19,"tag":20,"props":3822,"children":3824},{"id":3823},"the-full-pipeline",[3825],{"type":25,"value":3826},"The Full Pipeline",{"type":19,"tag":128,"props":3828,"children":3830},{"code":3829},"Code Push\n  ↓\n├─→ Lint & Format\n├─→ Unit Tests (parallel)\n├─→ Integration Tests (parallel)\n├─→ Build Artifact\n├─→ Visual Regression Tests\n├─→ Lighthouse Performance Check\n├─→ Security Scanning\n├─→ Deploy Preview (for PRs)\n└─→ Deploy Production (on main)\n",[3831],{"type":19,"tag":133,"props":3832,"children":3833},{"__ignoreMap":7},[3834],{"type":25,"value":3829},{"type":19,"tag":20,"props":3836,"children":3838},{"id":3837},"github-actions-workflow",[3839],{"type":25,"value":3840},"GitHub Actions Workflow",{"type":19,"tag":128,"props":3842,"children":3847},{"code":3843,"language":3844,"meta":7,"className":3845},"name: Frontend CI\u002FCD\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main, develop]\n\nenv:\n  NODE_VERSION: '18'\n  PNPM_VERSION: '8'\n\njobs:\n  # Setup\n  setup:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      \n      - uses: pnpm\u002Faction-setup@v2\n        with:\n          version: ${{ env.PNPM_VERSION }}\n      \n      - uses: actions\u002Fsetup-node@v3\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: 'pnpm'\n      \n      - run: pnpm install --frozen-lockfile\n  \n  # Lint\n  lint:\n    needs: setup\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: 'pnpm'\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm lint\n  \n  # Unit Tests (Parallel)\n  test-unit:\n    needs: setup\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        shard: [1, 2, 3, 4]\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: 'pnpm'\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm test:unit -- --shard=${{ matrix.shard }}\u002F4\n  \n  # Integration Tests\n  test-integration:\n    needs: setup\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        image: postgres:15\n        env:\n          POSTGRES_PASSWORD: postgres\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n        ports:\n          - 5432:5432\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm test:integration\n        env:\n          DATABASE_URL: postgresql:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftest\n  \n  # Build\n  build:\n    needs: setup\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      - run: pnpm build\n      - uses: actions\u002Fupload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n  \n  # Visual Regression Tests\n  visual-regression:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      \n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      - run: pnpm dlx playwright install --with-deps\n      - run: pnpm test:visual\n      \n      - uses: actions\u002Fupload-artifact@v3\n        if: failure()\n        with:\n          name: visual-regression-report\n          path: test-results\u002F\n  \n  # Performance Check\n  lighthouse:\n    needs: build\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      \n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      - uses: treosh\u002Flighthouse-ci-action@v9\n        with:\n          uploadArtifacts: true\n          configPath: '.\u002Flighthouserc.json'\n  \n  # Security Scanning\n  security:\n    needs: setup\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      \n      # npm audit\n      - run: npm audit --audit-level=moderate\n      \n      # Snyk scanning\n      - uses: snyk\u002Factions\u002Fsetup@master\n      - run: snyk test --severity-threshold=high\n        env:\n          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}\n      \n      # OWASP dependency check\n      - uses: dependency-check\u002FDependency-Check_Action@main\n        with:\n          project: 'Frontend'\n          path: '.'\n          format: 'SARIF'\n          args: >\n            --enableExperimental\n            --enableRetired\n  \n  # Deploy Preview (on PR)\n  deploy-preview:\n    needs: [lint, test-unit, build, lighthouse]\n    if: github.event_name == 'pull_request'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      - uses: netlify\u002Factions\u002Fcli@master\n        with:\n          args: deploy --dir=dist\n        env:\n          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}\n          NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}\n      \n      - uses: actions\u002Fgithub-script@v6\n        with:\n          script: |\n            github.rest.issues.createComment({\n              issue_number: context.issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              body: `Preview deployed: [View](https:\u002F\u002Fdeploy-preview-${{ github.event.number }}.netlify.app)`\n            })\n  \n  # Deploy Production (on main)\n  deploy-production:\n    needs: [lint, test-unit, build, lighthouse, security]\n    if: github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n        with:\n          fetch-depth: 0\n      \n      - uses: actions\u002Fdownload-artifact@v3\n        with:\n          name: build-artifact\n          path: dist\n      \n      # Semantic release\n      - uses: pnpm\u002Faction-setup@v2\n      - uses: actions\u002Fsetup-node@v3\n      - run: pnpm install --frozen-lockfile\n      \n      - uses: cycjimmy\u002Fsemantic-release-action@v3\n        id: semantic\n        with:\n          branches: |\n            [\n              '+([0-9])?(.{+([0-9]),x}).x',\n              'main',\n              'develop',\n              {\n                name: 'next',\n                prerelease: true\n              }\n            ]\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n      \n      # Deploy to Vercel\n      - uses: amondnet\u002Fvercel-action@v20\n        if: steps.semantic.outputs.new_release_published == 'true'\n        with:\n          vercel-token: ${{ secrets.VERCEL_TOKEN }}\n          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}\n          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}\n          working-directory: .\u002Fdist\n      \n      # Notify Slack\n      - uses: slackapi\u002Fslack-github-action@v1.24.0\n        with:\n          payload: |\n            {\n              \"text\": \"✅ Production deployment successful\",\n              \"blocks\": [\n                {\n                  \"type\": \"section\",\n                  \"text\": {\n                    \"type\": \"mrkdwn\",\n                    \"text\": \"*Production Deployed*\\n*Version:* ${{ steps.semantic.outputs.new_release_version }}\\n*Commit:* ${{ github.sha }}\"\n                  }\n                }\n              ]\n            }\n        env:\n          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}\n","yaml",[3846],"language-yaml",[3848],{"type":19,"tag":133,"props":3849,"children":3850},{"__ignoreMap":7},[3851],{"type":25,"value":3843},{"type":19,"tag":20,"props":3853,"children":3855},{"id":3854},"key-features-explained",[3856],{"type":25,"value":3857},"Key Features Explained",{"type":19,"tag":121,"props":3859,"children":3861},{"id":3860},"_1-parallel-job-execution",[3862],{"type":25,"value":3863},"1. Parallel Job Execution",{"type":19,"tag":28,"props":3865,"children":3866},{},[3867],{"type":25,"value":3868},"Tests run simultaneously instead of sequentially:",{"type":19,"tag":128,"props":3870,"children":3873},{"code":3871,"language":3844,"meta":7,"className":3872},"strategy:\n  matrix:\n    shard: [1, 2, 3, 4]\nrun: pnpm test -- --shard=${{ matrix.shard }}\u002F4\n",[3846],[3874],{"type":19,"tag":133,"props":3875,"children":3876},{"__ignoreMap":7},[3877],{"type":25,"value":3871},{"type":19,"tag":28,"props":3879,"children":3880},{},[3881],{"type":25,"value":3882},"Reduces CI time from 12 minutes → 4 minutes.",{"type":19,"tag":121,"props":3884,"children":3886},{"id":3885},"_2-dependency-caching",[3887],{"type":25,"value":3888},"2. Dependency Caching",{"type":19,"tag":128,"props":3890,"children":3893},{"code":3891,"language":3844,"meta":7,"className":3892},"- uses: actions\u002Fsetup-node@v3\n  with:\n    node-version: ${{ env.NODE_VERSION }}\n    cache: 'pnpm'\n",[3846],[3894],{"type":19,"tag":133,"props":3895,"children":3896},{"__ignoreMap":7},[3897],{"type":25,"value":3891},{"type":19,"tag":28,"props":3899,"children":3900},{},[3901,3903,3909],{"type":25,"value":3902},"Skips ",{"type":19,"tag":133,"props":3904,"children":3906},{"className":3905},[],[3907],{"type":25,"value":3908},"pnpm install",{"type":25,"value":3910}," if dependencies haven't changed. Saves 1-2 minutes per run.",{"type":19,"tag":121,"props":3912,"children":3914},{"id":3913},"_3-conditional-deployment",[3915],{"type":25,"value":3916},"3. Conditional Deployment",{"type":19,"tag":128,"props":3918,"children":3921},{"code":3919,"language":3844,"meta":7,"className":3920},"if: github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n",[3846],[3922],{"type":19,"tag":133,"props":3923,"children":3924},{"__ignoreMap":7},[3925],{"type":25,"value":3919},{"type":19,"tag":28,"props":3927,"children":3928},{},[3929],{"type":25,"value":3930},"Only deploy to production on main branch pushes. PRs get preview deployments.",{"type":19,"tag":121,"props":3932,"children":3934},{"id":3933},"_4-semantic-versioning",[3935],{"type":25,"value":3936},"4. Semantic Versioning",{"type":19,"tag":128,"props":3938,"children":3941},{"code":3939,"language":3844,"meta":7,"className":3940},"- uses: cycjimmy\u002Fsemantic-release-action@v3\n",[3846],[3942],{"type":19,"tag":133,"props":3943,"children":3944},{"__ignoreMap":7},[3945],{"type":25,"value":3939},{"type":19,"tag":28,"props":3947,"children":3948},{},[3949],{"type":25,"value":3950},"Automatically:",{"type":19,"tag":50,"props":3952,"children":3953},{},[3954,3959,3964,3969,3974],{"type":19,"tag":38,"props":3955,"children":3956},{},[3957],{"type":25,"value":3958},"Analyzes commits (feat → minor, fix → patch)",{"type":19,"tag":38,"props":3960,"children":3961},{},[3962],{"type":25,"value":3963},"Bumps version in package.json",{"type":19,"tag":38,"props":3965,"children":3966},{},[3967],{"type":25,"value":3968},"Creates GitHub release",{"type":19,"tag":38,"props":3970,"children":3971},{},[3972],{"type":25,"value":3973},"Publishes to npm",{"type":19,"tag":38,"props":3975,"children":3976},{},[3977],{"type":25,"value":3978},"Generates changelog",{"type":19,"tag":28,"props":3980,"children":3981},{},[3982],{"type":25,"value":3983},"One less manual step.",{"type":19,"tag":121,"props":3985,"children":3987},{"id":3986},"_5-performance-monitoring",[3988],{"type":25,"value":3989},"5. Performance Monitoring",{"type":19,"tag":128,"props":3991,"children":3994},{"code":3992,"language":3844,"meta":7,"className":3993},"- uses: treosh\u002Flighthouse-ci-action@v9\n",[3846],[3995],{"type":19,"tag":133,"props":3996,"children":3997},{"__ignoreMap":7},[3998],{"type":25,"value":3992},{"type":19,"tag":28,"props":4000,"children":4001},{},[4002],{"type":25,"value":4003},"Blocks deployment if performance degrades. Ensures Lighthouse score stays above threshold.",{"type":19,"tag":20,"props":4005,"children":4007},{"id":4006},"caching-strategy",[4008],{"type":25,"value":4009},"Caching Strategy",{"type":19,"tag":128,"props":4011,"children":4014},{"code":4012,"language":3844,"meta":7,"className":4013},"# .github\u002Fworkflows\u002Fci.yml\ncache-strategy:\n  - cache: node_modules\n    key: ${{ runner.os }}-pnpm-${{ hashFiles('**\u002Fpnpm-lock.yaml') }}\n    restore-keys: ${{ runner.os }}-pnpm-\n  \n  - cache: .next\n    key: ${{ runner.os }}-next-${{ hashFiles('**\u002Fpackage-lock.json') }}\n    restore-keys: ${{ runner.os }}-next-\n  \n  - cache: .turbo\n    key: ${{ runner.os }}-turbo-${{ hashFiles('**\u002Fpnpm-lock.yaml') }}\n    restore-keys: ${{ runner.os }}-turbo-\n",[3846],[4015],{"type":19,"tag":133,"props":4016,"children":4017},{"__ignoreMap":7},[4018],{"type":25,"value":4012},{"type":19,"tag":20,"props":4020,"children":4022},{"id":4021},"results",[4023],{"type":25,"value":4024},"Results",{"type":19,"tag":28,"props":4026,"children":4027},{},[4028],{"type":25,"value":4029},"What a pipeline like this typically changes (illustrative, not measured claims): CI time drops from tens of minutes to single digits, deploys go from a weekly event to a routine one, and security scanning stops depending on someone remembering to run it.",{"type":19,"tag":28,"props":4031,"children":4032},{},[4033,4035,4040],{"type":25,"value":4034},"My verified production result from building this kind of pipeline at Ordant: ",{"type":19,"tag":42,"props":4036,"children":4037},{},[4038],{"type":25,"value":4039},"merge-to-deploy time down 40%, with 99.9% uptime",{"type":25,"value":1882},{"type":19,"tag":20,"props":4042,"children":4044},{"id":4043},"cost-optimization",[4045],{"type":25,"value":4046},"Cost Optimization",{"type":19,"tag":28,"props":4048,"children":4049},{},[4050],{"type":25,"value":4051},"Worked example at GitHub Actions list pricing ($0.008\u002Fminute per job):",{"type":19,"tag":28,"props":4053,"children":4054},{},[4055],{"type":25,"value":4056},"A 5-minute pipeline with 6 parallel jobs = 30 job-minutes = $0.24\u002Frun.",{"type":19,"tag":28,"props":4058,"children":4059},{},[4060],{"type":25,"value":4061},"At 30 runs\u002Fday, that's $7.20\u002Fday ≈ $216\u002Fmonth.",{"type":19,"tag":28,"props":4063,"children":4064},{},[4065],{"type":25,"value":4066},"Worth it for the bugs prevented and deployment speed gained.",{"type":19,"tag":20,"props":4068,"children":4070},{"id":4069},"pro-tips",[4071],{"type":25,"value":4072},"Pro Tips",{"type":19,"tag":34,"props":4074,"children":4075},{},[4076,4086,4096,4106,4116],{"type":19,"tag":38,"props":4077,"children":4078},{},[4079,4084],{"type":19,"tag":42,"props":4080,"children":4081},{},[4082],{"type":25,"value":4083},"Cache aggressively",{"type":25,"value":4085}," — saves 1-3 minutes per run",{"type":19,"tag":38,"props":4087,"children":4088},{},[4089,4094],{"type":19,"tag":42,"props":4090,"children":4091},{},[4092],{"type":25,"value":4093},"Run tests in parallel",{"type":25,"value":4095}," — hardware is cheaper than time",{"type":19,"tag":38,"props":4097,"children":4098},{},[4099,4104],{"type":19,"tag":42,"props":4100,"children":4101},{},[4102],{"type":25,"value":4103},"Fail fast",{"type":25,"value":4105}," — lint before tests, tests before build",{"type":19,"tag":38,"props":4107,"children":4108},{},[4109,4114],{"type":19,"tag":42,"props":4110,"children":4111},{},[4112],{"type":25,"value":4113},"Monitor performance",{"type":25,"value":4115}," — performance regression is a deployment blocker",{"type":19,"tag":38,"props":4117,"children":4118},{},[4119,4124],{"type":19,"tag":42,"props":4120,"children":4121},{},[4122],{"type":25,"value":4123},"Automate versioning",{"type":25,"value":4125}," — semantic-release removes guesswork",{"type":19,"tag":28,"props":4127,"children":4128},{},[4129],{"type":25,"value":4130},"Good CI\u002FCD is a force multiplier. Invest in it early.",{"title":7,"searchDepth":681,"depth":681,"links":4132},[4133,4134,4135,4136,4143,4144,4145,4146],{"id":3731,"depth":681,"text":3734},{"id":3823,"depth":681,"text":3826},{"id":3837,"depth":681,"text":3840},{"id":3854,"depth":681,"text":3857,"children":4137},[4138,4139,4140,4141,4142],{"id":3860,"depth":687,"text":3863},{"id":3885,"depth":687,"text":3888},{"id":3913,"depth":687,"text":3916},{"id":3933,"depth":687,"text":3936},{"id":3986,"depth":687,"text":3989},{"id":4006,"depth":681,"text":4009},{"id":4021,"depth":681,"text":4024},{"id":4043,"depth":681,"text":4046},{"id":4069,"depth":681,"text":4072},"content:blog:advanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows",{"_path":4151,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":4152,"description":4153,"date":4154,"readingTime":1827,"tags":4155,"featured":6,"body":4157,"_type":708,"_id":5006,"_source":710,"_file":5007,"_stem":5008,"_extension":713},"\u002Fblog\u002Faccessibility-engineering-in-vue","Accessibility Engineering in Vue: Building for Everyone (WCAG 2.1 AA)","Comprehensive guide to building accessible Vue applications. Learn ARIA, keyboard navigation, semantic HTML, color contrast, screen reader support, and automated testing.","2024-05-20",[4156,723],"Accessibility",{"type":16,"children":4158,"toc":4993},[4159,4165,4170,4175,4218,4223,4229,4234,4267,4278,4284,4289,4298,4303,4418,4424,4429,4438,4443,4523,4529,4534,4543,4549,4554,4563,4568,4586,4592,4597,4606,4611,4663,4669,4678,4683,4712,4718,4723,4732,4741,4746,4755,4761,4766,4954,4960,4965,4988],{"type":19,"tag":20,"props":4160,"children":4162},{"id":4161},"why-accessibility-matters",[4163],{"type":25,"value":4164},"Why Accessibility Matters",{"type":19,"tag":28,"props":4166,"children":4167},{},[4168],{"type":25,"value":4169},"15% of the global population has disabilities. That's 1 billion people.",{"type":19,"tag":28,"props":4171,"children":4172},{},[4173],{"type":25,"value":4174},"For businesses:",{"type":19,"tag":50,"props":4176,"children":4177},{},[4178,4188,4198,4208],{"type":19,"tag":38,"props":4179,"children":4180},{},[4181,4186],{"type":19,"tag":42,"props":4182,"children":4183},{},[4184],{"type":25,"value":4185},"Legal",{"type":25,"value":4187},": WCAG compliance required in many jurisdictions (US, UK, EU)",{"type":19,"tag":38,"props":4189,"children":4190},{},[4191,4196],{"type":19,"tag":42,"props":4192,"children":4193},{},[4194],{"type":25,"value":4195},"Market",{"type":25,"value":4197},": Accessible sites reach 1 billion+ users",{"type":19,"tag":38,"props":4199,"children":4200},{},[4201,4206],{"type":19,"tag":42,"props":4202,"children":4203},{},[4204],{"type":25,"value":4205},"SEO",{"type":25,"value":4207},": Google rewards accessible sites",{"type":19,"tag":38,"props":4209,"children":4210},{},[4211,4216],{"type":19,"tag":42,"props":4212,"children":4213},{},[4214],{"type":25,"value":4215},"UX",{"type":25,"value":4217},": Good accessibility is good UX for everyone (captions help in noisy environments)",{"type":19,"tag":28,"props":4219,"children":4220},{},[4221],{"type":25,"value":4222},"Accessibility isn't a feature. It's a requirement.",{"type":19,"tag":20,"props":4224,"children":4226},{"id":4225},"web-content-accessibility-guidelines-wcag-21",[4227],{"type":25,"value":4228},"Web Content Accessibility Guidelines (WCAG) 2.1",{"type":19,"tag":28,"props":4230,"children":4231},{},[4232],{"type":25,"value":4233},"The standard has three levels:",{"type":19,"tag":50,"props":4235,"children":4236},{},[4237,4247,4257],{"type":19,"tag":38,"props":4238,"children":4239},{},[4240,4245],{"type":19,"tag":42,"props":4241,"children":4242},{},[4243],{"type":25,"value":4244},"A",{"type":25,"value":4246},": Minimum compliance",{"type":19,"tag":38,"props":4248,"children":4249},{},[4250,4255],{"type":19,"tag":42,"props":4251,"children":4252},{},[4253],{"type":25,"value":4254},"AA",{"type":25,"value":4256},": Recommended (what most sites aim for)",{"type":19,"tag":38,"props":4258,"children":4259},{},[4260,4265],{"type":19,"tag":42,"props":4261,"children":4262},{},[4263],{"type":25,"value":4264},"AAA",{"type":25,"value":4266},": Enhanced (nice to have)",{"type":19,"tag":28,"props":4268,"children":4269},{},[4270,4272,4276],{"type":25,"value":4271},"We'll focus on ",{"type":19,"tag":42,"props":4273,"children":4274},{},[4275],{"type":25,"value":4254},{"type":25,"value":4277},", the legal standard in most places.",{"type":19,"tag":20,"props":4279,"children":4281},{"id":4280},"semantic-html",[4282],{"type":25,"value":4283},"Semantic HTML",{"type":19,"tag":28,"props":4285,"children":4286},{},[4287],{"type":25,"value":4288},"The foundation of accessibility:",{"type":19,"tag":128,"props":4290,"children":4293},{"className":4291,"code":4292,"language":297,"meta":7},[299],"\u003C!-- ❌ Bad: Non-semantic HTML -->\n\u003Cdiv class=\"header\">\n  \u003Cdiv class=\"heading\">My Blog\u003C\u002Fdiv>\n  \u003Cdiv class=\"navigation\">\n    \u003Cdiv class=\"nav-item\">\u003Ca href=\"\u002F\">Home\u003C\u002Fa>\u003C\u002Fdiv>\n    \u003Cdiv class=\"nav-item\">\u003Ca href=\"\u002Fblog\">Blog\u003C\u002Fa>\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\n\u003Cdiv class=\"main\">\n  \u003Cdiv class=\"article\">\n    \u003Cdiv class=\"title\">Post Title\u003C\u002Fdiv>\n    \u003Cdiv class=\"content\">Post content...\u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Fdiv>\n\n\u003C!-- ✅ Good: Semantic HTML -->\n\u003Cheader>\n  \u003Ch1>My Blog\u003C\u002Fh1>\n  \u003Cnav>\n    \u003Cul>\n      \u003Cli>\u003Ca href=\"\u002F\">Home\u003C\u002Fa>\u003C\u002Fli>\n      \u003Cli>\u003Ca href=\"\u002Fblog\">Blog\u003C\u002Fa>\u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fnav>\n\u003C\u002Fheader>\n\n\u003Cmain>\n  \u003Carticle>\n    \u003Ch2>Post Title\u003C\u002Fh2>\n    \u003Cp>Post content...\u003C\u002Fp>\n  \u003C\u002Farticle>\n\u003C\u002Fmain>\n",[4294],{"type":19,"tag":133,"props":4295,"children":4296},{"__ignoreMap":7},[4297],{"type":25,"value":4292},{"type":19,"tag":28,"props":4299,"children":4300},{},[4301],{"type":25,"value":4302},"Semantic elements:",{"type":19,"tag":50,"props":4304,"children":4305},{},[4306,4351,4370,4381,4392],{"type":19,"tag":38,"props":4307,"children":4308},{},[4309,4315,4317,4323,4324,4330,4331,4337,4338,4344,4345],{"type":19,"tag":133,"props":4310,"children":4312},{"className":4311},[],[4313],{"type":25,"value":4314},"\u003Cheader>",{"type":25,"value":4316},", ",{"type":19,"tag":133,"props":4318,"children":4320},{"className":4319},[],[4321],{"type":25,"value":4322},"\u003Cnav>",{"type":25,"value":4316},{"type":19,"tag":133,"props":4325,"children":4327},{"className":4326},[],[4328],{"type":25,"value":4329},"\u003Cmain>",{"type":25,"value":4316},{"type":19,"tag":133,"props":4332,"children":4334},{"className":4333},[],[4335],{"type":25,"value":4336},"\u003Carticle>",{"type":25,"value":4316},{"type":19,"tag":133,"props":4339,"children":4341},{"className":4340},[],[4342],{"type":25,"value":4343},"\u003Csection>",{"type":25,"value":4316},{"type":19,"tag":133,"props":4346,"children":4348},{"className":4347},[],[4349],{"type":25,"value":4350},"\u003Cfooter>",{"type":19,"tag":38,"props":4352,"children":4353},{},[4354,4360,4362,4368],{"type":19,"tag":133,"props":4355,"children":4357},{"className":4356},[],[4358],{"type":25,"value":4359},"\u003Ch1>",{"type":25,"value":4361}," - ",{"type":19,"tag":133,"props":4363,"children":4365},{"className":4364},[],[4366],{"type":25,"value":4367},"\u003Ch6>",{"type":25,"value":4369}," for headings (never skip levels)",{"type":19,"tag":38,"props":4371,"children":4372},{},[4373,4379],{"type":19,"tag":133,"props":4374,"children":4376},{"className":4375},[],[4377],{"type":25,"value":4378},"\u003Cbutton>",{"type":25,"value":4380}," for interactive elements",{"type":19,"tag":38,"props":4382,"children":4383},{},[4384,4390],{"type":19,"tag":133,"props":4385,"children":4387},{"className":4386},[],[4388],{"type":25,"value":4389},"\u003Clabel>",{"type":25,"value":4391}," for form inputs",{"type":19,"tag":38,"props":4393,"children":4394},{},[4395,4401,4403,4409,4410,4416],{"type":19,"tag":133,"props":4396,"children":4398},{"className":4397},[],[4399],{"type":25,"value":4400},"\u003Ctable>",{"type":25,"value":4402}," for tabular data (with ",{"type":19,"tag":133,"props":4404,"children":4406},{"className":4405},[],[4407],{"type":25,"value":4408},"\u003Cthead>",{"type":25,"value":4316},{"type":19,"tag":133,"props":4411,"children":4413},{"className":4412},[],[4414],{"type":25,"value":4415},"\u003Ctbody>",{"type":25,"value":4417},")",{"type":19,"tag":20,"props":4419,"children":4421},{"id":4420},"aria-accessible-rich-internet-applications",[4422],{"type":25,"value":4423},"ARIA (Accessible Rich Internet Applications)",{"type":19,"tag":28,"props":4425,"children":4426},{},[4427],{"type":25,"value":4428},"Use ARIA when HTML alone can't describe the UI:",{"type":19,"tag":128,"props":4430,"children":4433},{"className":4431,"code":4432,"language":297,"meta":7},[299],"\u003Cscript setup lang=\"ts\">\nconst isOpen = ref(false)\nconst buttonRef = ref\u003CHTMLButtonElement>()\nconst menuRef = ref\u003CHTMLUListElement>()\n\nconst toggleMenu = () => {\n  isOpen.value = !isOpen.value\n  if (isOpen.value) {\n    menuRef.value?.focus()\n  }\n}\n\nconst closeMenu = () => {\n  isOpen.value = false\n  buttonRef.value?.focus()\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ARIA roles, states, and properties -->\n  \u003Cdiv class=\"dropdown\">\n    \u003Cbutton\n      ref=\"buttonRef\"\n      :aria-expanded=\"isOpen\"\n      aria-haspopup=\"menu\"\n      @click=\"toggleMenu\"\n    >\n      Menu\n    \u003C\u002Fbutton>\n\n    \u003Cul\n      v-if=\"isOpen\"\n      ref=\"menuRef\"\n      role=\"menu\"\n      class=\"dropdown-menu\"\n      @keydown.esc=\"closeMenu\"\n    >\n      \u003Cli role=\"none\">\n        \u003Ca role=\"menuitem\" href=\"#home\" @click=\"closeMenu\">Home\u003C\u002Fa>\n      \u003C\u002Fli>\n      \u003Cli role=\"none\">\n        \u003Ca role=\"menuitem\" href=\"#about\" @click=\"closeMenu\">About\u003C\u002Fa>\n      \u003C\u002Fli>\n      \u003Cli role=\"separator\" aria-label=\"Section divider\" \u002F>\n      \u003Cli role=\"none\">\n        \u003Ca role=\"menuitem\" href=\"#contact\" @click=\"closeMenu\">Contact\u003C\u002Fa>\n      \u003C\u002Fli>\n    \u003C\u002Ful>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[4434],{"type":19,"tag":133,"props":4435,"children":4436},{"__ignoreMap":7},[4437],{"type":25,"value":4432},{"type":19,"tag":28,"props":4439,"children":4440},{},[4441],{"type":25,"value":4442},"Key ARIA attributes:",{"type":19,"tag":50,"props":4444,"children":4445},{},[4446,4457,4468,4479,4490,4501,4512],{"type":19,"tag":38,"props":4447,"children":4448},{},[4449,4455],{"type":19,"tag":133,"props":4450,"children":4452},{"className":4451},[],[4453],{"type":25,"value":4454},"aria-label",{"type":25,"value":4456}," — Label for screen readers",{"type":19,"tag":38,"props":4458,"children":4459},{},[4460,4466],{"type":19,"tag":133,"props":4461,"children":4463},{"className":4462},[],[4464],{"type":25,"value":4465},"aria-labelledby",{"type":25,"value":4467}," — Reference element that labels this one",{"type":19,"tag":38,"props":4469,"children":4470},{},[4471,4477],{"type":19,"tag":133,"props":4472,"children":4474},{"className":4473},[],[4475],{"type":25,"value":4476},"aria-describedby",{"type":25,"value":4478}," — Reference element that describes this one",{"type":19,"tag":38,"props":4480,"children":4481},{},[4482,4488],{"type":19,"tag":133,"props":4483,"children":4485},{"className":4484},[],[4486],{"type":25,"value":4487},"aria-expanded",{"type":25,"value":4489}," — Whether collapsible element is open",{"type":19,"tag":38,"props":4491,"children":4492},{},[4493,4499],{"type":19,"tag":133,"props":4494,"children":4496},{"className":4495},[],[4497],{"type":25,"value":4498},"aria-hidden",{"type":25,"value":4500}," — Hide from screen readers (for decorative elements)",{"type":19,"tag":38,"props":4502,"children":4503},{},[4504,4510],{"type":19,"tag":133,"props":4505,"children":4507},{"className":4506},[],[4508],{"type":25,"value":4509},"aria-live",{"type":25,"value":4511}," — Announce changes dynamically",{"type":19,"tag":38,"props":4513,"children":4514},{},[4515,4521],{"type":19,"tag":133,"props":4516,"children":4518},{"className":4517},[],[4519],{"type":25,"value":4520},"role",{"type":25,"value":4522}," — Define element purpose when semantic HTML won't work",{"type":19,"tag":20,"props":4524,"children":4526},{"id":4525},"keyboard-navigation",[4527],{"type":25,"value":4528},"Keyboard Navigation",{"type":19,"tag":28,"props":4530,"children":4531},{},[4532],{"type":25,"value":4533},"Many users navigate by keyboard only. Every interactive element must be keyboard accessible:",{"type":19,"tag":128,"props":4535,"children":4538},{"className":4536,"code":4537,"language":297,"meta":7},[299],"\u003Cscript setup lang=\"ts\">\n\u002F\u002F Trap focus in modal\nconst trapFocus = (event: KeyboardEvent) => {\n  if (event.key !== 'Tab') return\n\n  const focusableElements = document.querySelectorAll(\n    'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'\n  )\n  \n  const firstElement = focusableElements[0] as HTMLElement\n  const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement\n\n  if (event.shiftKey && document.activeElement === firstElement) {\n    event.preventDefault()\n    lastElement.focus()\n  } else if (!event.shiftKey && document.activeElement === lastElement) {\n    event.preventDefault()\n    firstElement.focus()\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- Make elements focusable -->\n  \u003Cdiv\n    role=\"dialog\"\n    @keydown=\"trapFocus\"\n    @keydown.escape=\"closeModal\"\n  >\n    \u003Ch2 id=\"modal-title\">Confirm Action\u003C\u002Fh2>\n    \n    \u003C!-- Visible focus indicator -->\n    \u003Cbutton @click=\"confirm\" class=\"focus-visible:ring-2\">\n      Confirm\n    \u003C\u002Fbutton>\n    \n    \u003Cbutton @click=\"cancel\" class=\"focus-visible:ring-2\">\n      Cancel\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n\u002F* Always show focus indicator -->\n:focus-visible {\n  outline: 2px solid var(--accent);\n  outline-offset: 2px;\n}\n\n\u002F* Don't hide focus on keyboard users -->\nbutton:focus-visible {\n  outline: inherit;\n}\n\n\u002F* But hide for mouse users if desired *\u002F\nbutton:focus:not(:focus-visible) {\n  outline: none;\n}\n\u003C\u002Fstyle>\n",[4539],{"type":19,"tag":133,"props":4540,"children":4541},{"__ignoreMap":7},[4542],{"type":25,"value":4537},{"type":19,"tag":20,"props":4544,"children":4546},{"id":4545},"color-contrast",[4547],{"type":25,"value":4548},"Color Contrast",{"type":19,"tag":28,"props":4550,"children":4551},{},[4552],{"type":25,"value":4553},"Text must have sufficient contrast ratio:",{"type":19,"tag":128,"props":4555,"children":4558},{"className":4556,"code":4557,"language":297,"meta":7},[299],"\u003Cstyle scoped>\n\u002F* WCAG AA requires:\n   - Normal text: 4.5:1\n   - Large text: 3:1\n*\u002F\n\n.text-primary {\n  color: #000;\n  background: #fff;\n  \u002F* 21:1 contrast ✅ *\u002F\n}\n\n.text-secondary {\n  color: #555;\n  background: #fff;\n  \u002F* 8.59:1 contrast ✅ *\u002F\n}\n\n.text-tertiary {\n  color: #999;\n  background: #fff;\n  \u002F* 3.74:1 contrast ❌ *\u002F\n}\n\n\u002F* Test with: https:\u002F\u002Fwebaim.org\u002Fresources\u002Fcontrastchecker\u002F *\u002F\n\u003C\u002Fstyle>\n",[4559],{"type":19,"tag":133,"props":4560,"children":4561},{"__ignoreMap":7},[4562],{"type":25,"value":4557},{"type":19,"tag":28,"props":4564,"children":4565},{},[4566],{"type":25,"value":4567},"Use tools to check:",{"type":19,"tag":50,"props":4569,"children":4570},{},[4571,4576,4581],{"type":19,"tag":38,"props":4572,"children":4573},{},[4574],{"type":25,"value":4575},"WebAIM Contrast Checker",{"type":19,"tag":38,"props":4577,"children":4578},{},[4579],{"type":25,"value":4580},"Accessible Colors",{"type":19,"tag":38,"props":4582,"children":4583},{},[4584],{"type":25,"value":4585},"Chrome DevTools (automatic detection)",{"type":19,"tag":20,"props":4587,"children":4589},{"id":4588},"form-accessibility",[4590],{"type":25,"value":4591},"Form Accessibility",{"type":19,"tag":28,"props":4593,"children":4594},{},[4595],{"type":25,"value":4596},"Forms are a common accessibility fail point:",{"type":19,"tag":128,"props":4598,"children":4601},{"className":4599,"code":4600,"language":297,"meta":7},[299],"\u003Ctemplate>\n  \u003Cform @submit.prevent=\"submit\">\n    \u003C!-- ✅ Properly labeled input -->\n    \u003Cdiv class=\"form-group\">\n      \u003Clabel for=\"email\">Email Address\u003C\u002Flabel>\n      \u003Cinput\n        id=\"email\"\n        type=\"email\"\n        required\n        aria-describedby=\"email-hint\"\n      \u002F>\n      \u003Csmall id=\"email-hint\">We'll never share your email\u003C\u002Fsmall>\n    \u003C\u002Fdiv>\n\n    \u003C!-- ✅ Error messages linked to input -->\n    \u003Cdiv class=\"form-group\">\n      \u003Clabel for=\"password\">Password\u003C\u002Flabel>\n      \u003Cinput\n        id=\"password\"\n        type=\"password\"\n        required\n        aria-invalid=\"password.invalid\"\n        :aria-describedby=\"password.invalid ? 'password-error' : undefined\"\n      \u002F>\n      \u003Cdiv v-if=\"password.invalid\" id=\"password-error\" role=\"alert\">\n        Password must be at least 8 characters\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003C!-- ✅ Radio buttons with fieldset -->\n    \u003Cfieldset>\n      \u003Clegend>Select your role\u003C\u002Flegend>\n      \u003Cdiv>\n        \u003Cinput id=\"admin\" type=\"radio\" name=\"role\" value=\"admin\" \u002F>\n        \u003Clabel for=\"admin\">Administrator\u003C\u002Flabel>\n      \u003C\u002Fdiv>\n      \u003Cdiv>\n        \u003Cinput id=\"user\" type=\"radio\" name=\"role\" value=\"user\" \u002F>\n        \u003Clabel for=\"user\">User\u003C\u002Flabel>\n      \u003C\u002Fdiv>\n    \u003C\u002Ffieldset>\n\n    \u003Cbutton type=\"submit\">Submit\u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[4602],{"type":19,"tag":133,"props":4603,"children":4604},{"__ignoreMap":7},[4605],{"type":25,"value":4600},{"type":19,"tag":28,"props":4607,"children":4608},{},[4609],{"type":25,"value":4610},"Key form patterns:",{"type":19,"tag":50,"props":4612,"children":4613},{},[4614,4624,4635,4645,4650],{"type":19,"tag":38,"props":4615,"children":4616},{},[4617,4619],{"type":25,"value":4618},"Every input has a ",{"type":19,"tag":133,"props":4620,"children":4622},{"className":4621},[],[4623],{"type":25,"value":4389},{"type":19,"tag":38,"props":4625,"children":4626},{},[4627,4629],{"type":25,"value":4628},"Labels are connected with ",{"type":19,"tag":133,"props":4630,"children":4632},{"className":4631},[],[4633],{"type":25,"value":4634},"for=\"id\"",{"type":19,"tag":38,"props":4636,"children":4637},{},[4638,4640],{"type":25,"value":4639},"Error messages use ",{"type":19,"tag":133,"props":4641,"children":4643},{"className":4642},[],[4644],{"type":25,"value":4476},{"type":19,"tag":38,"props":4646,"children":4647},{},[4648],{"type":25,"value":4649},"Fieldset groups related inputs",{"type":19,"tag":38,"props":4651,"children":4652},{},[4653,4655,4661],{"type":25,"value":4654},"Use ",{"type":19,"tag":133,"props":4656,"children":4658},{"className":4657},[],[4659],{"type":25,"value":4660},"aria-invalid",{"type":25,"value":4662}," for validation states",{"type":19,"tag":20,"props":4664,"children":4666},{"id":4665},"images-media",[4667],{"type":25,"value":4668},"Images & Media",{"type":19,"tag":128,"props":4670,"children":4673},{"className":4671,"code":4672,"language":297,"meta":7},[299],"\u003Ctemplate>\n  \u003C!-- ✅ Descriptive alt text -->\n  \u003Cimg\n    src=\"hero.jpg\"\n    alt=\"Sunset over mountains with golden light reflecting on water\"\n  \u002F>\n\n  \u003C!-- ❌ Vague alt text -->\n  \u003Cimg src=\"photo.jpg\" alt=\"Photo\" \u002F>\n\n  \u003C!-- Icons with labels -->\n  \u003Cbutton aria-label=\"Close menu\">\n    \u003Csvg>\u003C!-- close icon -->\u003C\u002Fsvg>\n  \u003C\u002Fbutton>\n\n  \u003C!-- Videos with captions -->\n  \u003Cvideo controls>\n    \u003Csource src=\"video.mp4\" type=\"video\u002Fmp4\" \u002F>\n    \u003Ctrack kind=\"captions\" src=\"captions.vtt\" srclang=\"en\" \u002F>\n  \u003C\u002Fvideo>\n\u003C\u002Ftemplate>\n",[4674],{"type":19,"tag":133,"props":4675,"children":4676},{"__ignoreMap":7},[4677],{"type":25,"value":4672},{"type":19,"tag":28,"props":4679,"children":4680},{},[4681],{"type":25,"value":4682},"Alt text guidelines:",{"type":19,"tag":50,"props":4684,"children":4685},{},[4686,4691,4702,4707],{"type":19,"tag":38,"props":4687,"children":4688},{},[4689],{"type":25,"value":4690},"Describe the content, not \"image of\"",{"type":19,"tag":38,"props":4692,"children":4693},{},[4694,4696],{"type":25,"value":4695},"Decorative images get ",{"type":19,"tag":133,"props":4697,"children":4699},{"className":4698},[],[4700],{"type":25,"value":4701},"alt=\"\"",{"type":19,"tag":38,"props":4703,"children":4704},{},[4705],{"type":25,"value":4706},"Icons need aria-label if no visible text",{"type":19,"tag":38,"props":4708,"children":4709},{},[4710],{"type":25,"value":4711},"Keep under 125 characters",{"type":19,"tag":20,"props":4713,"children":4715},{"id":4714},"automated-testing",[4716],{"type":25,"value":4717},"Automated Testing",{"type":19,"tag":28,"props":4719,"children":4720},{},[4721],{"type":25,"value":4722},"Use axe for automated accessibility testing:",{"type":19,"tag":128,"props":4724,"children":4727},{"className":4725,"code":4726,"language":156,"meta":7},[158],"npm install -D @axe-core\u002Fplaywright jest-axe\n",[4728],{"type":19,"tag":133,"props":4729,"children":4730},{"__ignoreMap":7},[4731],{"type":25,"value":4726},{"type":19,"tag":128,"props":4733,"children":4736},{"className":4734,"code":4735,"language":172,"meta":7},[174],"\u002F\u002F __tests__\u002Faccessibility.spec.ts\nimport { test, expect } from '@playwright\u002Ftest'\nimport { injectAxe, checkA11y } from 'axe-playwright'\n\ntest('homepage is accessible', async ({ page }) => {\n  await page.goto('http:\u002F\u002Flocalhost:3000')\n  await injectAxe(page)\n  await checkA11y(page)\n})\n",[4737],{"type":19,"tag":133,"props":4738,"children":4739},{"__ignoreMap":7},[4740],{"type":25,"value":4735},{"type":19,"tag":28,"props":4742,"children":4743},{},[4744],{"type":25,"value":4745},"Jest testing:",{"type":19,"tag":128,"props":4747,"children":4750},{"className":4748,"code":4749,"language":172,"meta":7},[174],"import { render } from '@vue\u002Ftest-utils'\nimport { axe, toHaveNoViolations } from 'jest-axe'\nimport Button from '.\u002FButton.vue'\n\nexpect.extend(toHaveNoViolations)\n\nit('should not have accessibility violations', async () => {\n  const { container } = render(Button)\n  const results = await axe(container)\n  expect(results).toHaveNoViolations()\n})\n",[4751],{"type":19,"tag":133,"props":4752,"children":4753},{"__ignoreMap":7},[4754],{"type":25,"value":4749},{"type":19,"tag":20,"props":4756,"children":4758},{"id":4757},"accessibility-checklist",[4759],{"type":25,"value":4760},"Accessibility Checklist",{"type":19,"tag":28,"props":4762,"children":4763},{},[4764],{"type":25,"value":4765},"Before shipping:",{"type":19,"tag":50,"props":4767,"children":4770},{"className":4768},[4769],"contains-task-list",[4771,4789,4804,4819,4834,4849,4864,4879,4894,4909,4924,4939],{"type":19,"tag":38,"props":4772,"children":4775},{"className":4773},[4774],"task-list-item",[4776,4781,4783,4787],{"type":19,"tag":4777,"props":4778,"children":4780},"input",{"disabled":725,"type":4779},"checkbox",[],{"type":25,"value":4782}," ",{"type":19,"tag":42,"props":4784,"children":4785},{},[4786],{"type":25,"value":4283},{"type":25,"value":4788}," — use proper elements, not divs",{"type":19,"tag":38,"props":4790,"children":4792},{"className":4791},[4774],[4793,4796,4797,4802],{"type":19,"tag":4777,"props":4794,"children":4795},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4798,"children":4799},{},[4800],{"type":25,"value":4801},"Color contrast",{"type":25,"value":4803}," — 4.5:1 for normal text",{"type":19,"tag":38,"props":4805,"children":4807},{"className":4806},[4774],[4808,4811,4812,4817],{"type":19,"tag":4777,"props":4809,"children":4810},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4813,"children":4814},{},[4815],{"type":25,"value":4816},"Keyboard navigation",{"type":25,"value":4818}," — every interactive element accessible",{"type":19,"tag":38,"props":4820,"children":4822},{"className":4821},[4774],[4823,4826,4827,4832],{"type":19,"tag":4777,"props":4824,"children":4825},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4828,"children":4829},{},[4830],{"type":25,"value":4831},"Focus visible",{"type":25,"value":4833}," — clear focus indicator visible",{"type":19,"tag":38,"props":4835,"children":4837},{"className":4836},[4774],[4838,4841,4842,4847],{"type":19,"tag":4777,"props":4839,"children":4840},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4843,"children":4844},{},[4845],{"type":25,"value":4846},"Forms labeled",{"type":25,"value":4848}," — all inputs have labels",{"type":19,"tag":38,"props":4850,"children":4852},{"className":4851},[4774],[4853,4856,4857,4862],{"type":19,"tag":4777,"props":4854,"children":4855},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4858,"children":4859},{},[4860],{"type":25,"value":4861},"Images have alt text",{"type":25,"value":4863}," — descriptive, not vague",{"type":19,"tag":38,"props":4865,"children":4867},{"className":4866},[4774],[4868,4871,4872,4877],{"type":19,"tag":4777,"props":4869,"children":4870},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4873,"children":4874},{},[4875],{"type":25,"value":4876},"Headings logical",{"type":25,"value":4878}," — h1 → h2 → h3 (no skipping)",{"type":19,"tag":38,"props":4880,"children":4882},{"className":4881},[4774],[4883,4886,4887,4892],{"type":19,"tag":4777,"props":4884,"children":4885},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4888,"children":4889},{},[4890],{"type":25,"value":4891},"ARIA appropriate",{"type":25,"value":4893}," — only when necessary",{"type":19,"tag":38,"props":4895,"children":4897},{"className":4896},[4774],[4898,4901,4902,4907],{"type":19,"tag":4777,"props":4899,"children":4900},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4903,"children":4904},{},[4905],{"type":25,"value":4906},"No flash",{"type":25,"value":4908}," — no content flashing > 3 times\u002Fsecond",{"type":19,"tag":38,"props":4910,"children":4912},{"className":4911},[4774],[4913,4916,4917,4922],{"type":19,"tag":4777,"props":4914,"children":4915},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4918,"children":4919},{},[4920],{"type":25,"value":4921},"Responsive text",{"type":25,"value":4923}," — readable at 200% zoom",{"type":19,"tag":38,"props":4925,"children":4927},{"className":4926},[4774],[4928,4931,4932,4937],{"type":19,"tag":4777,"props":4929,"children":4930},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4933,"children":4934},{},[4935],{"type":25,"value":4936},"Mobile accessible",{"type":25,"value":4938}," — touch targets 48px minimum",{"type":19,"tag":38,"props":4940,"children":4942},{"className":4941},[4774],[4943,4946,4947,4952],{"type":19,"tag":4777,"props":4944,"children":4945},{"disabled":725,"type":4779},[],{"type":25,"value":4782},{"type":19,"tag":42,"props":4948,"children":4949},{},[4950],{"type":25,"value":4951},"Automated tests pass",{"type":25,"value":4953}," — axe, jest-axe",{"type":19,"tag":20,"props":4955,"children":4957},{"id":4956},"real-impact",[4958],{"type":25,"value":4959},"Real Impact",{"type":19,"tag":28,"props":4961,"children":4962},{},[4963],{"type":25,"value":4964},"After implementing accessibility:",{"type":19,"tag":50,"props":4966,"children":4967},{},[4968,4973,4978,4983],{"type":19,"tag":38,"props":4969,"children":4970},{},[4971],{"type":25,"value":4972},"Accessibility score: 40 → 92\u002F100 (Lighthouse)",{"type":19,"tag":38,"props":4974,"children":4975},{},[4976],{"type":25,"value":4977},"Users with assistive tech: +8% traffic",{"type":19,"tag":38,"props":4979,"children":4980},{},[4981],{"type":25,"value":4982},"SEO improvement: +15% in search ranking",{"type":19,"tag":38,"props":4984,"children":4985},{},[4986],{"type":25,"value":4987},"General UX improvement: better for everyone",{"type":19,"tag":28,"props":4989,"children":4990},{},[4991],{"type":25,"value":4992},"Accessibility isn't a nice-to-have. It's engineering excellence.",{"title":7,"searchDepth":681,"depth":681,"links":4994},[4995,4996,4997,4998,4999,5000,5001,5002,5003,5004,5005],{"id":4161,"depth":681,"text":4164},{"id":4225,"depth":681,"text":4228},{"id":4280,"depth":681,"text":4283},{"id":4420,"depth":681,"text":4423},{"id":4525,"depth":681,"text":4528},{"id":4545,"depth":681,"text":4548},{"id":4588,"depth":681,"text":4591},{"id":4665,"depth":681,"text":4668},{"id":4714,"depth":681,"text":4717},{"id":4757,"depth":681,"text":4760},{"id":4956,"depth":681,"text":4959},"content:blog:accessibility-engineering-in-vue.md","blog\u002Faccessibility-engineering-in-vue.md","blog\u002Faccessibility-engineering-in-vue",{"_path":5010,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":5011,"description":5012,"date":5013,"readingTime":11,"tags":5014,"featured":6,"body":5015,"_type":708,"_id":5469,"_source":710,"_file":5470,"_stem":5471,"_extension":713},"\u002Fblog\u002Fhow-to-structure-enterprise-vue-applications","How to Structure Enterprise Vue Applications: Scalable Folder Architecture","Architecture guide for large Vue applications. Learn folder structure, dependency injection, composables patterns, naming conventions, and scaling to 100K+ lines of code.","2024-05-05",[724,723],{"type":16,"children":5016,"toc":5457},[5017,5023,5028,5038,5046,5051,5061,5069,5074,5084,5092,5097,5103,5115,5123,5127,5184,5190,5195,5204,5209,5218,5223,5232,5238,5243,5252,5257,5266,5272,5277,5286,5290,5299,5305,5310,5319,5324,5330,5339,5345,5350,5358,5364,5369,5377,5383,5452],{"type":19,"tag":20,"props":5018,"children":5020},{"id":5019},"the-folder-structure-problem",[5021],{"type":25,"value":5022},"The Folder Structure Problem",{"type":19,"tag":28,"props":5024,"children":5025},{},[5026],{"type":25,"value":5027},"I've seen three patterns:",{"type":19,"tag":28,"props":5029,"children":5030},{},[5031,5036],{"type":19,"tag":42,"props":5032,"children":5033},{},[5034],{"type":25,"value":5035},"Chaos Model",{"type":25,"value":5037}," (what startups do):",{"type":19,"tag":128,"props":5039,"children":5041},{"code":5040},"src\u002F\n├── components\u002F\n│   ├── Button.vue\n│   ├── Modal.vue\n│   ├── UserCard.vue\n│   └── SomeRandomComponent.vue\n├── views\u002F\n│   ├── Home.vue\n│   └── Dashboard.vue\n└── utils.js\n",[5042],{"type":19,"tag":133,"props":5043,"children":5044},{"__ignoreMap":7},[5045],{"type":25,"value":5040},{"type":19,"tag":28,"props":5047,"children":5048},{},[5049],{"type":25,"value":5050},"After 6 months, nobody remembers where anything is.",{"type":19,"tag":28,"props":5052,"children":5053},{},[5054,5059],{"type":19,"tag":42,"props":5055,"children":5056},{},[5057],{"type":25,"value":5058},"Rails Model",{"type":25,"value":5060}," (what some teams copy):",{"type":19,"tag":128,"props":5062,"children":5064},{"code":5063},"src\u002F\n├── views\u002F\n├── components\u002F\n├── composables\u002F\n├── stores\u002F\n├── utils\u002F\n└── services\u002F\n",[5065],{"type":19,"tag":133,"props":5066,"children":5067},{"__ignoreMap":7},[5068],{"type":25,"value":5063},{"type":19,"tag":28,"props":5070,"children":5071},{},[5072],{"type":25,"value":5073},"Better, but still unclear what belongs where.",{"type":19,"tag":28,"props":5075,"children":5076},{},[5077,5082],{"type":19,"tag":42,"props":5078,"children":5079},{},[5080],{"type":25,"value":5081},"Domain-Driven Model",{"type":25,"value":5083}," (what scales):",{"type":19,"tag":128,"props":5085,"children":5087},{"code":5086},"src\u002F\n├── domains\u002F\n│   ├── users\u002F\n│   │   ├── components\u002F\n│   │   ├── composables\u002F\n│   │   ├── stores\u002F\n│   │   ├── types\u002F\n│   │   └── api.ts\n│   ├── billing\u002F\n│   └── products\u002F\n├── shared\u002F\n│   ├── components\u002F\n│   ├── composables\u002F\n│   ├── types\u002F\n│   └── utils\u002F\n└── app.vue\n",[5088],{"type":19,"tag":133,"props":5089,"children":5090},{"__ignoreMap":7},[5091],{"type":25,"value":5086},{"type":19,"tag":28,"props":5093,"children":5094},{},[5095],{"type":25,"value":5096},"We'll focus on the domain-driven model.",{"type":19,"tag":20,"props":5098,"children":5100},{"id":5099},"the-domain-driven-structure",[5101],{"type":25,"value":5102},"The Domain-Driven Structure",{"type":19,"tag":28,"props":5104,"children":5105},{},[5106,5108,5113],{"type":25,"value":5107},"Organize by ",{"type":19,"tag":42,"props":5109,"children":5110},{},[5111],{"type":25,"value":5112},"business domain",{"type":25,"value":5114},", not technical layer:",{"type":19,"tag":128,"props":5116,"children":5118},{"code":5117},"src\u002F\n├── domains\u002F\n│   │\n│   ├── users\u002F\n│   │   ├── api.ts              # API calls for users\n│   │   ├── types.ts            # User types (User, CreateUserDTO)\n│   │   ├── composables\u002F\n│   │   │   ├── useUser.ts      # Single user fetch\u002Fupdate\n│   │   │   ├── useUserList.ts  # List with pagination\n│   │   │   └── useAuth.ts      # Authentication\n│   │   ├── stores\u002F\n│   │   │   └── userStore.ts    # Pinia store\n│   │   ├── components\u002F\n│   │   │   ├── UserCard.vue\n│   │   │   ├── UserForm.vue\n│   │   │   └── UserAvatar.vue\n│   │   ├── pages\u002F\n│   │   │   ├── UserDetail.vue\n│   │   │   └── UserList.vue\n│   │   └── index.ts            # Public API\n│   │\n│   ├── billing\u002F\n│   │   ├── api.ts\n│   │   ├── types.ts            # Invoice, Subscription types\n│   │   ├── composables\u002F\n│   │   ├── stores\u002F\n│   │   ├── components\u002F\n│   │   ├── pages\u002F\n│   │   └── index.ts\n│   │\n│   └── products\u002F\n│       ├── api.ts\n│       ├── types.ts\n│       ├── composables\u002F\n│       ├── stores\u002F\n│       ├── components\u002F\n│       ├── pages\u002F\n│       └── index.ts\n│\n├── shared\u002F\n│   ├── components\u002F             # Reusable UI (Button, Modal, etc.)\n│   │   ├── Button.vue\n│   │   ├── Modal.vue\n│   │   └── Pagination.vue\n│   ├── composables\u002F            # Shared logic (useLocalStorage, etc.)\n│   │   ├── useFetch.ts\n│   │   ├── useLocalStorage.ts\n│   │   └── useDebounce.ts\n│   ├── types\u002F\n│   │   ├── index.ts\n│   │   └── api.ts\n│   ├── utils\u002F\n│   │   ├── formatters.ts\n│   │   ├── validators.ts\n│   │   └── helpers.ts\n│   ├── stores\u002F\n│   │   └── appStore.ts         # Global app state\n│   ├── middleware\u002F\n│   │   ├── auth.ts\n│   │   └── logging.ts\n│   └── index.ts                # Barrel export\n│\n├── router\u002F\n│   ├── index.ts\n│   ├── guards.ts\n│   └── routes.ts\n│\n├── styles\u002F\n│   ├── main.css\n│   ├── variables.css\n│   └── utilities.css\n│\n├── plugins\u002F\n│   └── index.ts\n│\n└── app.vue\n",[5119],{"type":19,"tag":133,"props":5120,"children":5121},{"__ignoreMap":7},[5122],{"type":25,"value":5117},{"type":19,"tag":28,"props":5124,"children":5125},{},[5126],{"type":25,"value":1912},{"type":19,"tag":50,"props":5128,"children":5129},{},[5130,5140,5164,5174],{"type":19,"tag":38,"props":5131,"children":5132},{},[5133,5138],{"type":19,"tag":42,"props":5134,"children":5135},{},[5136],{"type":25,"value":5137},"Feature cohesion",{"type":25,"value":5139}," — all code for \"users\" is in one place",{"type":19,"tag":38,"props":5141,"children":5142},{},[5143,5148,5150,5156,5158],{"type":19,"tag":42,"props":5144,"children":5145},{},[5146],{"type":25,"value":5147},"Independent shipping",{"type":25,"value":5149}," — one team owns ",{"type":19,"tag":133,"props":5151,"children":5153},{"className":5152},[],[5154],{"type":25,"value":5155},"domains\u002Fusers",{"type":25,"value":5157},", another owns ",{"type":19,"tag":133,"props":5159,"children":5161},{"className":5160},[],[5162],{"type":25,"value":5163},"domains\u002Fbilling",{"type":19,"tag":38,"props":5165,"children":5166},{},[5167,5172],{"type":19,"tag":42,"props":5168,"children":5169},{},[5170],{"type":25,"value":5171},"Clear dependencies",{"type":25,"value":5173}," — easy to see what each domain needs",{"type":19,"tag":38,"props":5175,"children":5176},{},[5177,5182],{"type":19,"tag":42,"props":5178,"children":5179},{},[5180],{"type":25,"value":5181},"Scaling",{"type":25,"value":5183}," — add new domains without touching existing ones",{"type":19,"tag":20,"props":5185,"children":5187},{"id":5186},"the-barrel-export-pattern",[5188],{"type":25,"value":5189},"The Barrel Export Pattern",{"type":19,"tag":28,"props":5191,"children":5192},{},[5193],{"type":25,"value":5194},"Each domain exports a public API:",{"type":19,"tag":128,"props":5196,"children":5199},{"code":5197,"language":172,"meta":7,"className":5198},"\u002F\u002F domains\u002Fusers\u002Findex.ts\nexport * from '.\u002Ftypes'\nexport { default as useUser } from '.\u002Fcomposables\u002FuseUser'\nexport { default as useUserList } from '.\u002Fcomposables\u002FuseUserList'\nexport { default as UserCard } from '.\u002Fcomponents\u002FUserCard.vue'\nexport { default as UserForm } from '.\u002Fcomponents\u002FUserForm.vue'\nexport { useUserStore } from '.\u002Fstores\u002FuserStore'\nexport * as userApi from '.\u002Fapi'\n",[174],[5200],{"type":19,"tag":133,"props":5201,"children":5202},{"__ignoreMap":7},[5203],{"type":25,"value":5197},{"type":19,"tag":28,"props":5205,"children":5206},{},[5207],{"type":25,"value":5208},"Usage:",{"type":19,"tag":128,"props":5210,"children":5213},{"code":5211,"language":172,"meta":7,"className":5212},"\u002F\u002F Importing from domain is clean\nimport { useUser, UserCard, userApi } from '@\u002Fdomains\u002Fusers'\n\n\u002F\u002F Instead of scattered imports\nimport useUser from '@\u002Fdomains\u002Fusers\u002Fcomposables\u002FuseUser'\nimport UserCard from '@\u002Fdomains\u002Fusers\u002Fcomponents\u002FUserCard.vue'\nimport * as userApi from '@\u002Fdomains\u002Fusers\u002Fapi'\n",[174],[5214],{"type":19,"tag":133,"props":5215,"children":5216},{"__ignoreMap":7},[5217],{"type":25,"value":5211},{"type":19,"tag":28,"props":5219,"children":5220},{},[5221],{"type":25,"value":5222},"Enforce this with eslint:",{"type":19,"tag":128,"props":5224,"children":5227},{"code":5225,"language":1006,"meta":7,"className":5226},"\u002F\u002F .eslintrc.js\n{\n  rules: {\n    'import\u002Fno-restricted-paths': [\n      'error',\n      {\n        zones: [\n          {\n            target: '.\u002Fsrc\u002Fdomains\u002Fusers',\n            from: '.\u002Fsrc\u002Fdomains\u002Fbilling',\n            message: 'Users domain cannot import from Billing domain'\n          }\n        ]\n      }\n    ]\n  }\n}\n",[1004],[5228],{"type":19,"tag":133,"props":5229,"children":5230},{"__ignoreMap":7},[5231],{"type":25,"value":5225},{"type":19,"tag":20,"props":5233,"children":5235},{"id":5234},"composables-the-shared-logic",[5236],{"type":25,"value":5237},"Composables: The Shared Logic",{"type":19,"tag":28,"props":5239,"children":5240},{},[5241],{"type":25,"value":5242},"Composables are where reusable logic lives:",{"type":19,"tag":128,"props":5244,"children":5247},{"code":5245,"language":172,"meta":7,"className":5246},"\u002F\u002F domains\u002Fusers\u002Fcomposables\u002FuseUser.ts\nimport { ref, computed } from 'vue'\nimport type { User } from '..\u002Ftypes'\nimport * as userApi from '..\u002Fapi'\n\nexport const useUser = (userId: string) => {\n  const user = ref\u003CUser | null>(null)\n  const loading = ref(false)\n  const error = ref\u003Cstring | null>(null)\n\n  const fetchUser = async () => {\n    loading.value = true\n    try {\n      user.value = await userApi.getUser(userId)\n      error.value = null\n    } catch (err) {\n      error.value = (err as Error).message\n      user.value = null\n    } finally {\n      loading.value = false\n    }\n  }\n\n  const updateUser = async (updates: Partial\u003CUser>) => {\n    try {\n      user.value = await userApi.updateUser(userId, updates)\n      error.value = null\n    } catch (err) {\n      error.value = (err as Error).message\n    }\n  }\n\n  onMounted(() => fetchUser())\n\n  return {\n    user: readonly(user),\n    loading: readonly(loading),\n    error: readonly(error),\n    fetchUser,\n    updateUser\n  }\n}\n",[174],[5248],{"type":19,"tag":133,"props":5249,"children":5250},{"__ignoreMap":7},[5251],{"type":25,"value":5245},{"type":19,"tag":28,"props":5253,"children":5254},{},[5255],{"type":25,"value":5256},"Use it:",{"type":19,"tag":128,"props":5258,"children":5261},{"code":5259,"language":297,"meta":7,"className":5260},"\u003Cscript setup lang=\"ts\">\nimport { useUser } from '@\u002Fdomains\u002Fusers'\n\nconst route = useRoute()\nconst { user, loading, error } = useUser(route.params.id)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv v-if=\"loading\">Loading...\u003C\u002Fdiv>\n  \u003Cdiv v-else-if=\"error\">{{ error }}\u003C\u002Fdiv>\n  \u003Cdiv v-else>{{ user?.name }}\u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[299],[5262],{"type":19,"tag":133,"props":5263,"children":5264},{"__ignoreMap":7},[5265],{"type":25,"value":5259},{"type":19,"tag":20,"props":5267,"children":5269},{"id":5268},"dependency-injection",[5270],{"type":25,"value":5271},"Dependency Injection",{"type":19,"tag":28,"props":5273,"children":5274},{},[5275],{"type":25,"value":5276},"For large apps, inject dependencies instead of hardcoding:",{"type":19,"tag":128,"props":5278,"children":5281},{"code":5279,"language":172,"meta":7,"className":5280},"\u002F\u002F shared\u002Fservices\u002Fapi.ts\nclass ApiService {\n  private baseUrl: string\n\n  constructor(baseUrl: string) {\n    this.baseUrl = baseUrl\n  }\n\n  async get(path: string) {\n    return fetch(`${this.baseUrl}${path}`).then(r => r.json())\n  }\n}\n\n\u002F\u002F main.ts\nimport { createApp } from 'vue'\nimport ApiService from '.\u002Fshared\u002Fservices\u002Fapi'\n\nconst app = createApp(App)\n\napp.provide('api', new ApiService(import.meta.env.VITE_API_URL))\n\napp.mount('#app')\n",[174],[5282],{"type":19,"tag":133,"props":5283,"children":5284},{"__ignoreMap":7},[5285],{"type":25,"value":5279},{"type":19,"tag":28,"props":5287,"children":5288},{},[5289],{"type":25,"value":5256},{"type":19,"tag":128,"props":5291,"children":5294},{"code":5292,"language":172,"meta":7,"className":5293},"\u002F\u002F domains\u002Fusers\u002Fapi.ts\nimport { inject } from 'vue'\n\nexport const useUserApi = () => {\n  const api = inject\u003CApiService>('api')!\n  \n  return {\n    getUser: (id: string) => api.get(`\u002Fusers\u002F${id}`),\n    listUsers: () => api.get('\u002Fusers'),\n    createUser: (data: any) => api.post('\u002Fusers', data)\n  }\n}\n",[174],[5295],{"type":19,"tag":133,"props":5296,"children":5297},{"__ignoreMap":7},[5298],{"type":25,"value":5292},{"type":19,"tag":20,"props":5300,"children":5302},{"id":5301},"types-organization",[5303],{"type":25,"value":5304},"Types Organization",{"type":19,"tag":28,"props":5306,"children":5307},{},[5308],{"type":25,"value":5309},"Centralize types:",{"type":19,"tag":128,"props":5311,"children":5314},{"code":5312,"language":172,"meta":7,"className":5313},"\u002F\u002F domains\u002Fusers\u002Ftypes.ts\nexport interface User {\n  id: string\n  name: string\n  email: string\n  avatar?: string\n  role: 'user' | 'admin'\n  createdAt: string\n  updatedAt: string\n}\n\nexport interface CreateUserDTO {\n  name: string\n  email: string\n  password: string\n  role?: 'user' | 'admin'\n}\n\nexport interface UpdateUserDTO {\n  name?: string\n  email?: string\n  avatar?: string\n}\n\nexport interface UserListResponse {\n  data: User[]\n  total: number\n  page: number\n  pageSize: number\n}\n",[174],[5315],{"type":19,"tag":133,"props":5316,"children":5317},{"__ignoreMap":7},[5318],{"type":25,"value":5312},{"type":19,"tag":28,"props":5320,"children":5321},{},[5322],{"type":25,"value":5323},"Never use inline interfaces.",{"type":19,"tag":20,"props":5325,"children":5327},{"id":5326},"naming-conventions",[5328],{"type":25,"value":5329},"Naming Conventions",{"type":19,"tag":128,"props":5331,"children":5334},{"code":5332,"language":172,"meta":7,"className":5333},"\u002F\u002F Components: PascalCase\nUserCard.vue\nUserForm.vue\nCreateUserModal.vue\n\n\u002F\u002F Composables: useCamelCase\nuseUser.ts\nuseUserList.ts\nuseUserForm.ts\nuseDebounce.ts\n\n\u002F\u002F Stores: Store suffix\nuserStore.ts\nauthStore.ts\nappStore.ts\n\n\u002F\u002F API files: lowercase\napi.ts (or users-api.ts)\n\n\u002F\u002F Types: PascalCase\nUser.ts\nCreateUserDTO.ts\n\n\u002F\u002F Utils: camelCase, descriptive\nformatDate.ts\nvalidateEmail.ts\ncalculateDiscount.ts\n",[174],[5335],{"type":19,"tag":133,"props":5336,"children":5337},{"__ignoreMap":7},[5338],{"type":25,"value":5332},{"type":19,"tag":20,"props":5340,"children":5342},{"id":5341},"growing-the-structure",[5343],{"type":25,"value":5344},"Growing the Structure",{"type":19,"tag":28,"props":5346,"children":5347},{},[5348],{"type":25,"value":5349},"As you add more code, follow these patterns:",{"type":19,"tag":128,"props":5351,"children":5353},{"code":5352},"domains\u002Fusers\u002F (20 KB)\n  ├── api.ts\n  ├── types.ts\n  ├── composables\u002F (3 files)\n  ├── components\u002F (5 files)\n  └── pages\u002F (2 files)\n\n# When it gets too big (50+ KB), break it down:\n\ndomains\u002F\n├── users\u002F\n│   ├── list\u002F            # User listing feature\n│   ├── detail\u002F          # User detail\u002Fedit feature\n│   ├── auth\u002F            # Auth-specific logic\n│   └── shared\u002F          # Shared user types\u002Fapi\n",[5354],{"type":19,"tag":133,"props":5355,"children":5356},{"__ignoreMap":7},[5357],{"type":25,"value":5352},{"type":19,"tag":20,"props":5359,"children":5361},{"id":5360},"scaling-to-100k-lines",[5362],{"type":25,"value":5363},"Scaling to 100K+ Lines",{"type":19,"tag":28,"props":5365,"children":5366},{},[5367],{"type":25,"value":5368},"At scale, add:",{"type":19,"tag":128,"props":5370,"children":5372},{"code":5371},"src\u002F\n├── domains\u002F\n├── features\u002F            # Cross-domain workflows\n│   ├── onboarding\u002F      # Onboarding flow uses Users + Billing\n│   ├── migration\u002F       # Data migration uses multiple domains\n│   └── reporting\u002F\n├── shared\u002F\n├── router\u002F\n└── layouts\u002F             # Layout components\n",[5373],{"type":19,"tag":133,"props":5374,"children":5375},{"__ignoreMap":7},[5376],{"type":25,"value":5371},{"type":19,"tag":20,"props":5378,"children":5380},{"id":5379},"key-rules",[5381],{"type":25,"value":5382},"Key Rules",{"type":19,"tag":34,"props":5384,"children":5385},{},[5386,5396,5406,5416,5426,5442],{"type":19,"tag":38,"props":5387,"children":5388},{},[5389,5394],{"type":19,"tag":42,"props":5390,"children":5391},{},[5392],{"type":25,"value":5393},"Keep domains independent",{"type":25,"value":5395}," — Users domain can't import from Billing",{"type":19,"tag":38,"props":5397,"children":5398},{},[5399,5404],{"type":19,"tag":42,"props":5400,"children":5401},{},[5402],{"type":25,"value":5403},"Shared is truly shared",{"type":25,"value":5405}," — only reusable across domains",{"type":19,"tag":38,"props":5407,"children":5408},{},[5409,5414],{"type":19,"tag":42,"props":5410,"children":5411},{},[5412],{"type":25,"value":5413},"Use barrel exports",{"type":25,"value":5415}," — clean public APIs",{"type":19,"tag":38,"props":5417,"children":5418},{},[5419,5424],{"type":19,"tag":42,"props":5420,"children":5421},{},[5422],{"type":25,"value":5423},"Type everything",{"type":25,"value":5425}," — centralize types by domain",{"type":19,"tag":38,"props":5427,"children":5428},{},[5429,5434,5436],{"type":19,"tag":42,"props":5430,"children":5431},{},[5432],{"type":25,"value":5433},"One entry point",{"type":25,"value":5435}," — each domain exports from ",{"type":19,"tag":133,"props":5437,"children":5439},{"className":5438},[],[5440],{"type":25,"value":5441},"index.ts",{"type":19,"tag":38,"props":5443,"children":5444},{},[5445,5450],{"type":19,"tag":42,"props":5446,"children":5447},{},[5448],{"type":25,"value":5449},"Enforce with linting",{"type":25,"value":5451}," — catch violations early",{"type":19,"tag":28,"props":5453,"children":5454},{},[5455],{"type":25,"value":5456},"This structure scales from 10 engineers to 100. Your codebase will thank you.",{"title":7,"searchDepth":681,"depth":681,"links":5458},[5459,5460,5461,5462,5463,5464,5465,5466,5467,5468],{"id":5019,"depth":681,"text":5022},{"id":5099,"depth":681,"text":5102},{"id":5186,"depth":681,"text":5189},{"id":5234,"depth":681,"text":5237},{"id":5268,"depth":681,"text":5271},{"id":5301,"depth":681,"text":5304},{"id":5326,"depth":681,"text":5329},{"id":5341,"depth":681,"text":5344},{"id":5360,"depth":681,"text":5363},{"id":5379,"depth":681,"text":5382},"content:blog:how-to-structure-enterprise-vue-applications.md","blog\u002Fhow-to-structure-enterprise-vue-applications.md","blog\u002Fhow-to-structure-enterprise-vue-applications",{"_path":5473,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":5474,"description":5475,"date":5476,"updated":720,"readingTime":5477,"tags":5478,"featured":6,"body":5479,"_type":708,"_id":6203,"_source":710,"_file":6204,"_stem":6205,"_extension":713},"\u002Fblog\u002Freducing-bundle-size-in-large-vue-apps","Reducing Bundle Size in Large Vue Apps: A Strategic Guide","Strategic guide to bundle optimization. Learn tree-shaking, dynamic imports, dependency analysis, code splitting, and continuous monitoring.","2024-04-18",10,[13],{"type":16,"children":5480,"toc":6181},[5481,5487,5499,5511,5542,5547,5553,5558,5567,5572,5590,5595,5651,5657,5663,5672,5678,5687,5693,5702,5708,5717,5722,5740,5750,5756,5761,5770,5776,5785,5791,5800,5813,5818,5836,5842,5847,5856,5865,5870,5879,5884,5890,5896,5905,5911,5920,5926,5936,6031,6043,6061,6067,6120,6124,6176],{"type":19,"tag":20,"props":5482,"children":5484},{"id":5483},"the-bundle-problem",[5485],{"type":25,"value":5486},"The Bundle Problem",{"type":19,"tag":28,"props":5488,"children":5489},{},[5490,5492,5497],{"type":25,"value":5491},"Vue apps grow organically, and nobody watches the bundle until it hurts. Working on a clinical data platform, I shipped a bundle reduction of ",{"type":19,"tag":42,"props":5493,"children":5494},{},[5495],{"type":25,"value":5496},"35%",{"type":25,"value":5498}," using the approach in this guide — the strategy below is the repeatable part.",{"type":19,"tag":28,"props":5500,"children":5501},{},[5502,5504,5509],{"type":25,"value":5503},"To make the mechanics concrete, this guide walks through a representative large Vue app. ",{"type":19,"tag":42,"props":5505,"children":5506},{},[5507],{"type":25,"value":5508},"All specific sizes below are an illustrative example",{"type":25,"value":5510},", not measurements from one project:",{"type":19,"tag":50,"props":5512,"children":5513},{},[5514,5523,5532],{"type":19,"tag":38,"props":5515,"children":5516},{},[5517,5521],{"type":19,"tag":42,"props":5518,"children":5519},{},[5520],{"type":25,"value":1341},{"type":25,"value":5522},": 850 KB (gzipped: 280 KB)",{"type":19,"tag":38,"props":5524,"children":5525},{},[5526,5530],{"type":19,"tag":42,"props":5527,"children":5528},{},[5529],{"type":25,"value":1351},{"type":25,"value":5531},": 450 KB (gzipped: 150 KB)",{"type":19,"tag":38,"props":5533,"children":5534},{},[5535,5540],{"type":19,"tag":42,"props":5536,"children":5537},{},[5538],{"type":25,"value":5539},"Total",{"type":25,"value":5541},": 1.2 MB gzipped",{"type":19,"tag":28,"props":5543,"children":5544},{},[5545],{"type":25,"value":5546},"At 3G speeds (1 Mbps), that's 10+ seconds just for JavaScript.",{"type":19,"tag":20,"props":5548,"children":5550},{"id":5549},"step-1-analyze-the-damage",[5551],{"type":25,"value":5552},"Step 1: Analyze the Damage",{"type":19,"tag":28,"props":5554,"children":5555},{},[5556],{"type":25,"value":5557},"First, visualize where bytes are going:",{"type":19,"tag":128,"props":5559,"children":5562},{"className":5560,"code":5561,"language":156,"meta":7},[158],"npm install -D rollup-plugin-visualizer\n\n# vite.config.ts\nimport { visualizer } from 'rollup-plugin-visualizer'\n\nexport default {\n  plugins: [vue(), visualizer()]\n}\n\nnpm run build\n# Opens dist\u002Fstats.html — interactive bundle visualization\n",[5563],{"type":19,"tag":133,"props":5564,"children":5565},{"__ignoreMap":7},[5566],{"type":25,"value":5561},{"type":19,"tag":28,"props":5568,"children":5569},{},[5570],{"type":25,"value":5571},"You'll see:",{"type":19,"tag":50,"props":5573,"children":5574},{},[5575,5580,5585],{"type":19,"tag":38,"props":5576,"children":5577},{},[5578],{"type":25,"value":5579},"What packages are large",{"type":19,"tag":38,"props":5581,"children":5582},{},[5583],{"type":25,"value":5584},"What's imported but unused",{"type":19,"tag":38,"props":5586,"children":5587},{},[5588],{"type":25,"value":5589},"Duplicate dependencies at different versions",{"type":19,"tag":28,"props":5591,"children":5592},{},[5593],{"type":25,"value":5594},"A typical analysis reveals (illustrative example):",{"type":19,"tag":50,"props":5596,"children":5597},{},[5598,5609,5627,5638],{"type":19,"tag":38,"props":5599,"children":5600},{},[5601,5607],{"type":19,"tag":133,"props":5602,"children":5604},{"className":5603},[],[5605],{"type":25,"value":5606},"lodash-es",{"type":25,"value":5608},": 71 KB (for 3 functions actually used)",{"type":19,"tag":38,"props":5610,"children":5611},{},[5612,5618,5620,5626],{"type":19,"tag":133,"props":5613,"children":5615},{"className":5614},[],[5616],{"type":25,"value":5617},"moment",{"type":25,"value":5619},": 65 KB (replaced with ",{"type":19,"tag":133,"props":5621,"children":5623},{"className":5622},[],[5624],{"type":25,"value":5625},"date-fns",{"type":25,"value":4417},{"type":19,"tag":38,"props":5628,"children":5629},{},[5630,5636],{"type":19,"tag":133,"props":5631,"children":5633},{"className":5632},[],[5634],{"type":25,"value":5635},"chart.js",{"type":25,"value":5637},": 120 KB (only used on one page)",{"type":19,"tag":38,"props":5639,"children":5640},{},[5641,5643,5649],{"type":25,"value":5642},"Duplicate ",{"type":19,"tag":133,"props":5644,"children":5646},{"className":5645},[],[5647],{"type":25,"value":5648},"axios",{"type":25,"value":5650},": two versions imported by different packages",{"type":19,"tag":20,"props":5652,"children":5654},{"id":5653},"step-2-tree-shake-aggressively",[5655],{"type":25,"value":5656},"Step 2: Tree-Shake Aggressively",{"type":19,"tag":121,"props":5658,"children":5660},{"id":5659},"remove-unused-packages",[5661],{"type":25,"value":5662},"Remove Unused Packages",{"type":19,"tag":128,"props":5664,"children":5667},{"className":5665,"code":5666,"language":156,"meta":7},[158],"# Find unused dependencies\nnpm install -g depcheck\ndepcheck\n\n# Removes 50+ KB of dead code\nnpm uninstall unused-package\n",[5668],{"type":19,"tag":133,"props":5669,"children":5670},{"__ignoreMap":7},[5671],{"type":25,"value":5666},{"type":19,"tag":121,"props":5673,"children":5675},{"id":5674},"replace-heavy-packages",[5676],{"type":25,"value":5677},"Replace Heavy Packages",{"type":19,"tag":128,"props":5679,"children":5682},{"className":5680,"code":5681,"language":172,"meta":7},[174],"\u002F\u002F ❌ lodash-es (71 KB)\nimport { debounce, throttle, cloneDeep } from 'lodash-es'\n\n\u002F\u002F ✅ Use native JavaScript + small libraries\n\u002F\u002F Debounce (use native setTimeout)\nconst debounce = (fn: Function, delay: number) => {\n  let timeout: NodeJS.Timeout\n  return (...args: any[]) => {\n    clearTimeout(timeout)\n    timeout = setTimeout(() => fn(...args), delay)\n  }\n}\n\n\u002F\u002F Or use a lighter library\nimport { debounce } from 'radash' \u002F\u002F 8 KB vs 71 KB\n",[5683],{"type":19,"tag":133,"props":5684,"children":5685},{"__ignoreMap":7},[5686],{"type":25,"value":5681},{"type":19,"tag":121,"props":5688,"children":5690},{"id":5689},"dependency-consolidation",[5691],{"type":25,"value":5692},"Dependency Consolidation",{"type":19,"tag":128,"props":5694,"children":5697},{"className":5695,"code":5696,"language":172,"meta":7},[174],"\u002F\u002F ❌ Multiple date libraries\nimport { format } from 'date-fns'\nimport moment from 'moment' \u002F\u002F Someone else's code\nimport dayjs from 'dayjs' \u002F\u002F Yet another\n\n\u002F\u002F ✅ Choose one\nimport { format } from 'date-fns'\n\u002F\u002F date-fns: 30 KB (tree-shakeable) vs moment: 65 KB (not tree-shakeable)\n",[5698],{"type":19,"tag":133,"props":5699,"children":5700},{"__ignoreMap":7},[5701],{"type":25,"value":5696},{"type":19,"tag":121,"props":5703,"children":5705},{"id":5704},"build-config-for-tree-shaking",[5706],{"type":25,"value":5707},"Build Config for Tree-Shaking",{"type":19,"tag":128,"props":5709,"children":5712},{"className":5710,"code":5711,"language":172,"meta":7},[174],"\u002F\u002F vite.config.ts\nexport default defineConfig({\n  build: {\n    minify: 'terser',\n    terserOptions: {\n      compress: {\n        drop_console: true,\n        drop_debugger: true\n      }\n    },\n    rollupOptions: {\n      output: {\n        \u002F\u002F Manual code splitting\n        manualChunks: {\n          \u002F\u002F Separate large dependencies\n          'charts': ['chart.js', 'vue-chartjs'],\n          'markdown': ['markdown-it', 'highlight.js'],\n          'utils': ['lodash', 'date-fns'],\n          'vendor': ['vue', 'vue-router', 'pinia']\n        }\n      }\n    }\n  }\n})\n",[5713],{"type":19,"tag":133,"props":5714,"children":5715},{"__ignoreMap":7},[5716],{"type":25,"value":5711},{"type":19,"tag":28,"props":5718,"children":5719},{},[5720],{"type":25,"value":5721},"Results for the illustrative example app:",{"type":19,"tag":50,"props":5723,"children":5724},{},[5725,5730,5735],{"type":19,"tag":38,"props":5726,"children":5727},{},[5728],{"type":25,"value":5729},"Removed unused: -150 KB",{"type":19,"tag":38,"props":5731,"children":5732},{},[5733],{"type":25,"value":5734},"Replaced heavy packages: -120 KB",{"type":19,"tag":38,"props":5736,"children":5737},{},[5738],{"type":25,"value":5739},"Tree-shaking optimized: -40 KB",{"type":19,"tag":28,"props":5741,"children":5742},{},[5743,5748],{"type":19,"tag":42,"props":5744,"children":5745},{},[5746],{"type":25,"value":5747},"Total: -310 KB (27% reduction)",{"type":25,"value":5749}," — again, illustrative; the ratio between the three buckets is what generalizes.",{"type":19,"tag":20,"props":5751,"children":5753},{"id":5752},"step-3-smart-code-splitting",[5754],{"type":25,"value":5755},"Step 3: Smart Code Splitting",{"type":19,"tag":28,"props":5757,"children":5758},{},[5759],{"type":25,"value":5760},"Load code only when needed:",{"type":19,"tag":128,"props":5762,"children":5765},{"className":5763,"code":5764,"language":172,"meta":7},[174],"\u002F\u002F ❌ All routes imported upfront\nimport Home from '@\u002Fpages\u002FHome.vue'\nimport Dashboard from '@\u002Fpages\u002FDashboard.vue'\nimport Settings from '@\u002Fpages\u002FSettings.vue'\nimport Admin from '@\u002Fpages\u002FAdmin.vue'\n\nconst routes = [\n  { path: '\u002F', component: Home },\n  { path: '\u002Fdashboard', component: Dashboard },\n  { path: '\u002Fsettings', component: Settings },\n  { path: '\u002Fadmin', component: Admin }\n]\n\n\u002F\u002F ✅ Dynamic imports for routes\nconst routes = [\n  { path: '\u002F', component: () => import('@\u002Fpages\u002FHome.vue') },\n  { path: '\u002Fdashboard', component: () => import('@\u002Fpages\u002FDashboard.vue') },\n  { path: '\u002Fsettings', component: () => import('@\u002Fpages\u002FSettings.vue') },\n  { path: '\u002Fadmin', component: () => import('@\u002Fpages\u002FAdmin.vue') }\n]\n",[5766],{"type":19,"tag":133,"props":5767,"children":5768},{"__ignoreMap":7},[5769],{"type":25,"value":5764},{"type":19,"tag":121,"props":5771,"children":5773},{"id":5772},"split-heavy-features",[5774],{"type":25,"value":5775},"Split Heavy Features",{"type":19,"tag":128,"props":5777,"children":5780},{"className":5778,"code":5779,"language":172,"meta":7},[174],"\u002F\u002F ❌ Import editor everywhere (even if not used)\nimport RichEditor from '@\u002Fcomponents\u002FRichEditor.vue'\n\n\u002F\u002F ✅ Async only when needed\nconst RichEditor = defineAsyncComponent(\n  () => import('@\u002Fcomponents\u002FRichEditor.vue')\n)\n",[5781],{"type":19,"tag":133,"props":5782,"children":5783},{"__ignoreMap":7},[5784],{"type":25,"value":5779},{"type":19,"tag":121,"props":5786,"children":5788},{"id":5787},"webpack-magic-comment",[5789],{"type":25,"value":5790},"Webpack Magic Comment",{"type":19,"tag":128,"props":5792,"children":5795},{"className":5793,"code":5794,"language":172,"meta":7},[174],"\u002F\u002F Name the chunk for better debugging\nconst Dashboard = () =>\n  import(\n    \u002F* webpackChunkName: \"dashboard\" *\u002F\n    '@\u002Fpages\u002FDashboard.vue'\n  )\n",[5796],{"type":19,"tag":133,"props":5797,"children":5798},{"__ignoreMap":7},[5799],{"type":25,"value":5794},{"type":19,"tag":28,"props":5801,"children":5802},{},[5803,5805,5811],{"type":25,"value":5804},"Generates: ",{"type":19,"tag":133,"props":5806,"children":5808},{"className":5807},[],[5809],{"type":25,"value":5810},"dist\u002Fdashboard.abc123.js",{"type":25,"value":5812}," (clear what it contains)",{"type":19,"tag":28,"props":5814,"children":5815},{},[5816],{"type":25,"value":5817},"Results of code splitting (illustrative example):",{"type":19,"tag":50,"props":5819,"children":5820},{},[5821,5826,5831],{"type":19,"tag":38,"props":5822,"children":5823},{},[5824],{"type":25,"value":5825},"The main bundle shrinks to what the first route actually needs",{"type":19,"tag":38,"props":5827,"children":5828},{},[5829],{"type":25,"value":5830},"Lazy routes load in small on-demand chunks",{"type":19,"tag":38,"props":5832,"children":5833},{},[5834],{"type":25,"value":5835},"First page load stops paying for pages the user never visits",{"type":19,"tag":20,"props":5837,"children":5839},{"id":5838},"step-4-monitor-continuously",[5840],{"type":25,"value":5841},"Step 4: Monitor Continuously",{"type":19,"tag":28,"props":5843,"children":5844},{},[5845],{"type":25,"value":5846},"Prevent regressions:",{"type":19,"tag":128,"props":5848,"children":5851},{"className":5849,"code":5850,"language":156,"meta":7},[158],"npm install -D bundlesize\n",[5852],{"type":19,"tag":133,"props":5853,"children":5854},{"__ignoreMap":7},[5855],{"type":25,"value":5850},{"type":19,"tag":128,"props":5857,"children":5860},{"className":5858,"code":5859,"language":2179,"meta":7},[2181],"\u002F\u002F .bundlesize.json\n{\n  \"files\": [\n    {\n      \"path\": \".\u002Fdist\u002Findex.*.js\",\n      \"maxSize\": \"250kb\"\n    },\n    {\n      \"path\": \".\u002Fdist\u002F*.js\",\n      \"maxSize\": \"100kb\"\n    }\n  ]\n}\n",[5861],{"type":19,"tag":133,"props":5862,"children":5863},{"__ignoreMap":7},[5864],{"type":25,"value":5859},{"type":19,"tag":28,"props":5866,"children":5867},{},[5868],{"type":25,"value":5869},"GitHub Action:",{"type":19,"tag":128,"props":5871,"children":5874},{"className":5872,"code":5873,"language":3844,"meta":7},[3846],"# .github\u002Fworkflows\u002Fbundle-size.yml\nname: Bundle Size Check\n\non: [pull_request]\n\njobs:\n  bundle:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\u002Fcheckout@v3\n      - run: npm ci\n      - run: npm run build\n      - uses: bundlesize\u002Faction@v1\n        with:\n          files: .\u002Fdist\u002F**\u002F*.js\n          builtFile: dist\n",[5875],{"type":19,"tag":133,"props":5876,"children":5877},{"__ignoreMap":7},[5878],{"type":25,"value":5873},{"type":19,"tag":28,"props":5880,"children":5881},{},[5882],{"type":25,"value":5883},"Fails PR if bundles exceed limits.",{"type":19,"tag":20,"props":5885,"children":5887},{"id":5886},"step-5-optimize-imports",[5888],{"type":25,"value":5889},"Step 5: Optimize Imports",{"type":19,"tag":121,"props":5891,"children":5893},{"id":5892},"named-vs-default-exports",[5894],{"type":25,"value":5895},"Named vs Default Exports",{"type":19,"tag":128,"props":5897,"children":5900},{"className":5898,"code":5899,"language":172,"meta":7},[174],"\u002F\u002F ❌ Default exports (can't tree-shake)\nexport default { formatDate: ... }\nimport * as dateUtils from 'lib'\n\n\u002F\u002F ✅ Named exports (tree-shakeable)\nexport { formatDate, formatTime }\nimport { formatDate } from 'lib'\n",[5901],{"type":19,"tag":133,"props":5902,"children":5903},{"__ignoreMap":7},[5904],{"type":25,"value":5899},{"type":19,"tag":121,"props":5906,"children":5908},{"id":5907},"avoid-wildcard-imports",[5909],{"type":25,"value":5910},"Avoid Wildcard Imports",{"type":19,"tag":128,"props":5912,"children":5915},{"className":5913,"code":5914,"language":172,"meta":7},[174],"\u002F\u002F ❌ Imports everything\nimport * as utils from '@\u002Futils'\nutils.formatDate()\n\n\u002F\u002F ✅ Import only what's needed\nimport { formatDate } from '@\u002Futils'\nformatDate()\n",[5916],{"type":19,"tag":133,"props":5917,"children":5918},{"__ignoreMap":7},[5919],{"type":25,"value":5914},{"type":19,"tag":20,"props":5921,"children":5923},{"id":5922},"results-ongoing-monitoring",[5924],{"type":25,"value":5925},"Results & Ongoing Monitoring",{"type":19,"tag":28,"props":5927,"children":5928},{},[5929,5934],{"type":19,"tag":42,"props":5930,"children":5931},{},[5932],{"type":25,"value":5933},"Before & after for the illustrative example app",{"type":25,"value":5935}," (your ratios will vary; the direction won't):",{"type":19,"tag":478,"props":5937,"children":5938},{},[5939,5961],{"type":19,"tag":482,"props":5940,"children":5941},{},[5942],{"type":19,"tag":486,"props":5943,"children":5944},{},[5945,5949,5953,5957],{"type":19,"tag":490,"props":5946,"children":5947},{},[5948],{"type":25,"value":494},{"type":19,"tag":490,"props":5950,"children":5951},{},[5952],{"type":25,"value":499},{"type":19,"tag":490,"props":5954,"children":5955},{},[5956],{"type":25,"value":504},{"type":19,"tag":490,"props":5958,"children":5959},{},[5960],{"type":25,"value":1658},{"type":19,"tag":506,"props":5962,"children":5963},{},[5964,5987,6009],{"type":19,"tag":486,"props":5965,"children":5966},{},[5967,5972,5977,5982],{"type":19,"tag":513,"props":5968,"children":5969},{},[5970],{"type":25,"value":5971},"Main Bundle",{"type":19,"tag":513,"props":5973,"children":5974},{},[5975],{"type":25,"value":5976},"850 KB",{"type":19,"tag":513,"props":5978,"children":5979},{},[5980],{"type":25,"value":5981},"~300 KB",{"type":19,"tag":513,"props":5983,"children":5984},{},[5985],{"type":25,"value":5986},"large",{"type":19,"tag":486,"props":5988,"children":5989},{},[5990,5995,6000,6005],{"type":19,"tag":513,"props":5991,"children":5992},{},[5993],{"type":25,"value":5994},"Vendor Bundle",{"type":19,"tag":513,"props":5996,"children":5997},{},[5998],{"type":25,"value":5999},"450 KB",{"type":19,"tag":513,"props":6001,"children":6002},{},[6003],{"type":25,"value":6004},"~180 KB",{"type":19,"tag":513,"props":6006,"children":6007},{},[6008],{"type":25,"value":5986},{"type":19,"tag":486,"props":6010,"children":6011},{},[6012,6017,6022,6027],{"type":19,"tag":513,"props":6013,"children":6014},{},[6015],{"type":25,"value":6016},"First Paint (3G)",{"type":19,"tag":513,"props":6018,"children":6019},{},[6020],{"type":25,"value":6021},"~8s",{"type":19,"tag":513,"props":6023,"children":6024},{},[6025],{"type":25,"value":6026},"~2s",{"type":19,"tag":513,"props":6028,"children":6029},{},[6030],{"type":25,"value":5986},{"type":19,"tag":28,"props":6032,"children":6033},{},[6034,6036,6041],{"type":25,"value":6035},"In my own production use of this strategy, the verified outcome was a ",{"type":19,"tag":42,"props":6037,"children":6038},{},[6039],{"type":25,"value":6040},"35% bundle reduction",{"type":25,"value":6042},". Beyond raw size:",{"type":19,"tag":50,"props":6044,"children":6045},{},[6046,6051,6056],{"type":19,"tag":38,"props":6047,"children":6048},{},[6049],{"type":25,"value":6050},"Faster for all users, especially mobile",{"type":19,"tag":38,"props":6052,"children":6053},{},[6054],{"type":25,"value":6055},"Better SEO (Core Web Vitals improvement)",{"type":19,"tag":38,"props":6057,"children":6058},{},[6059],{"type":25,"value":6060},"Less battery drain on mobile",{"type":19,"tag":20,"props":6062,"children":6064},{"id":6063},"ongoing-strategy",[6065],{"type":25,"value":6066},"Ongoing Strategy",{"type":19,"tag":34,"props":6068,"children":6069},{},[6070,6080,6090,6100,6110],{"type":19,"tag":38,"props":6071,"children":6072},{},[6073,6078],{"type":19,"tag":42,"props":6074,"children":6075},{},[6076],{"type":25,"value":6077},"Weekly monitoring",{"type":25,"value":6079}," — bundlesize checks every PR",{"type":19,"tag":38,"props":6081,"children":6082},{},[6083,6088],{"type":19,"tag":42,"props":6084,"children":6085},{},[6086],{"type":25,"value":6087},"Quarterly audits",{"type":25,"value":6089}," — analyze with visualizer",{"type":19,"tag":38,"props":6091,"children":6092},{},[6093,6098],{"type":19,"tag":42,"props":6094,"children":6095},{},[6096],{"type":25,"value":6097},"Department rotation",{"type":25,"value":6099}," — one engineer owns bundle each month",{"type":19,"tag":38,"props":6101,"children":6102},{},[6103,6108],{"type":19,"tag":42,"props":6104,"children":6105},{},[6106],{"type":25,"value":6107},"Team education",{"type":25,"value":6109}," — awareness of bundle impact",{"type":19,"tag":38,"props":6111,"children":6112},{},[6113,6118],{"type":19,"tag":42,"props":6114,"children":6115},{},[6116],{"type":25,"value":6117},"Dependency reviews",{"type":25,"value":6119}," — approve new packages by size",{"type":19,"tag":20,"props":6121,"children":6122},{"id":4069},[6123],{"type":25,"value":4072},{"type":19,"tag":34,"props":6125,"children":6126},{},[6127,6137,6147,6157,6167],{"type":19,"tag":38,"props":6128,"children":6129},{},[6130,6135],{"type":19,"tag":42,"props":6131,"children":6132},{},[6133],{"type":25,"value":6134},"Lazy load routes by default",{"type":25,"value":6136}," — all routes should be dynamic",{"type":19,"tag":38,"props":6138,"children":6139},{},[6140,6145],{"type":19,"tag":42,"props":6141,"children":6142},{},[6143],{"type":25,"value":6144},"Split by feature",{"type":25,"value":6146}," — heavy features in separate chunks",{"type":19,"tag":38,"props":6148,"children":6149},{},[6150,6155],{"type":19,"tag":42,"props":6151,"children":6152},{},[6153],{"type":25,"value":6154},"Use es2015 module syntax",{"type":25,"value":6156}," — enables tree-shaking",{"type":19,"tag":38,"props":6158,"children":6159},{},[6160,6165],{"type":19,"tag":42,"props":6161,"children":6162},{},[6163],{"type":25,"value":6164},"Compress with Brotli",{"type":25,"value":6166}," — 15-20% smaller than gzip",{"type":19,"tag":38,"props":6168,"children":6169},{},[6170,6174],{"type":19,"tag":42,"props":6171,"children":6172},{},[6173],{"type":25,"value":1796},{"type":25,"value":6175}," — real user metrics matter most",{"type":19,"tag":28,"props":6177,"children":6178},{},[6179],{"type":25,"value":6180},"Bundle size affects every user, every time. It's worth optimizing.",{"title":7,"searchDepth":681,"depth":681,"links":6182},[6183,6184,6185,6191,6195,6196,6200,6201,6202],{"id":5483,"depth":681,"text":5486},{"id":5549,"depth":681,"text":5552},{"id":5653,"depth":681,"text":5656,"children":6186},[6187,6188,6189,6190],{"id":5659,"depth":687,"text":5662},{"id":5674,"depth":687,"text":5677},{"id":5689,"depth":687,"text":5692},{"id":5704,"depth":687,"text":5707},{"id":5752,"depth":681,"text":5755,"children":6192},[6193,6194],{"id":5772,"depth":687,"text":5775},{"id":5787,"depth":687,"text":5790},{"id":5838,"depth":681,"text":5841},{"id":5886,"depth":681,"text":5889,"children":6197},[6198,6199],{"id":5892,"depth":687,"text":5895},{"id":5907,"depth":687,"text":5910},{"id":5922,"depth":681,"text":5925},{"id":6063,"depth":681,"text":6066},{"id":4069,"depth":681,"text":4072},"content:blog:reducing-bundle-size-in-large-vue-apps.md","blog\u002Freducing-bundle-size-in-large-vue-apps.md","blog\u002Freducing-bundle-size-in-large-vue-apps",{"_path":6207,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":6208,"description":6209,"date":6210,"readingTime":5477,"tags":6211,"featured":6,"body":6214,"_type":708,"_id":6831,"_source":710,"_file":6832,"_stem":6833,"_extension":713},"\u002Fblog\u002Fremote-frontend-engineering-best-practices","Remote Frontend Engineering Best Practices: Async-First, Documented, Autonomous","Guide to being an effective remote frontend engineer. Covers async communication, documentation, code review culture, timezone management, and team autonomy principles.","2024-04-01",[6212,6213],"Remote","Culture",{"type":16,"children":6215,"toc":6806},[6216,6222,6227,6232,6254,6264,6270,6276,6281,6289,6294,6302,6307,6360,6366,6371,6379,6384,6390,6395,6400,6410,6415,6421,6426,6432,6441,6446,6452,6461,6467,6476,6482,6491,6497,6515,6521,6526,6534,6539,6544,6549,6554,6562,6567,6573,6578,6584,6589,6597,6603,6611,6617,6626,6632,6640,6646,6651,6704,6710,6715,6757,6763,6773,6778,6801],{"type":19,"tag":20,"props":6217,"children":6219},{"id":6218},"the-remote-frontend-engineer",[6220],{"type":25,"value":6221},"The Remote Frontend Engineer",{"type":19,"tag":28,"props":6223,"children":6224},{},[6225],{"type":25,"value":6226},"Remote work is now standard. Being a remote engineer requires a different mindset than co-located work.",{"type":19,"tag":28,"props":6228,"children":6229},{},[6230],{"type":25,"value":6231},"The difference:",{"type":19,"tag":50,"props":6233,"children":6234},{},[6235,6245],{"type":19,"tag":38,"props":6236,"children":6237},{},[6238,6243],{"type":19,"tag":42,"props":6239,"children":6240},{},[6241],{"type":25,"value":6242},"Co-located",{"type":25,"value":6244},": \"I'll ask Bob on Slack\"",{"type":19,"tag":38,"props":6246,"children":6247},{},[6248,6252],{"type":19,"tag":42,"props":6249,"children":6250},{},[6251],{"type":25,"value":6212},{"type":25,"value":6253},": \"Bob is asleep; I'll find the answer in docs\"",{"type":19,"tag":28,"props":6255,"children":6256},{},[6257,6259],{"type":25,"value":6258},"The principle: ",{"type":19,"tag":42,"props":6260,"children":6261},{},[6262],{"type":25,"value":6263},"Assume your teammates are asleep, in a meeting, or handling an emergency.",{"type":19,"tag":20,"props":6265,"children":6267},{"id":6266},"async-first-communication",[6268],{"type":25,"value":6269},"Async-First Communication",{"type":19,"tag":121,"props":6271,"children":6273},{"id":6272},"write-instead-of-chat",[6274],{"type":25,"value":6275},"Write Instead of Chat",{"type":19,"tag":28,"props":6277,"children":6278},{},[6279],{"type":25,"value":6280},"Bad:",{"type":19,"tag":128,"props":6282,"children":6284},{"code":6283},"You: hey are you around?\nYou: quick question about the Button component\nYou: the disabled state styling?\nYou: is it supposed to have opacity or a different color?\n(Bob sees messages 4 hours later, types a 2-word answer)\n",[6285],{"type":19,"tag":133,"props":6286,"children":6287},{"__ignoreMap":7},[6288],{"type":25,"value":6283},{"type":19,"tag":28,"props":6290,"children":6291},{},[6292],{"type":25,"value":6293},"Good:",{"type":19,"tag":128,"props":6295,"children":6297},{"code":6296},"Slack message in #frontend-design-system:\n\"Question about Button.vue disabled state:\n- Current: opacity: 0.5\n- Proposal: color: var(--color-disabled)\n- Context: new accessibility requirement\n- Decision needed by EOD\nReference: #PR-1234\"\n(Bob reads full context, gives detailed answer with reasoning)\n",[6298],{"type":19,"tag":133,"props":6299,"children":6300},{"__ignoreMap":7},[6301],{"type":25,"value":6296},{"type":19,"tag":28,"props":6303,"children":6304},{},[6305],{"type":25,"value":6306},"Include:",{"type":19,"tag":50,"props":6308,"children":6309},{},[6310,6320,6330,6340,6350],{"type":19,"tag":38,"props":6311,"children":6312},{},[6313,6318],{"type":19,"tag":42,"props":6314,"children":6315},{},[6316],{"type":25,"value":6317},"Context",{"type":25,"value":6319}," — what you're trying to do",{"type":19,"tag":38,"props":6321,"children":6322},{},[6323,6328],{"type":19,"tag":42,"props":6324,"children":6325},{},[6326],{"type":25,"value":6327},"What you've tried",{"type":25,"value":6329}," — don't ask before researching",{"type":19,"tag":38,"props":6331,"children":6332},{},[6333,6338],{"type":19,"tag":42,"props":6334,"children":6335},{},[6336],{"type":25,"value":6337},"Specific question",{"type":25,"value":6339}," — not \"does this work?\"",{"type":19,"tag":38,"props":6341,"children":6342},{},[6343,6348],{"type":19,"tag":42,"props":6344,"children":6345},{},[6346],{"type":25,"value":6347},"Deadline",{"type":25,"value":6349}," — when you need the answer",{"type":19,"tag":38,"props":6351,"children":6352},{},[6353,6358],{"type":19,"tag":42,"props":6354,"children":6355},{},[6356],{"type":25,"value":6357},"Links",{"type":25,"value":6359}," — PR\u002Fissue references",{"type":19,"tag":121,"props":6361,"children":6363},{"id":6362},"use-threads",[6364],{"type":25,"value":6365},"Use Threads",{"type":19,"tag":28,"props":6367,"children":6368},{},[6369],{"type":25,"value":6370},"Slack threading keeps conversations organized:",{"type":19,"tag":128,"props":6372,"children":6374},{"code":6373},"Message in #frontend channel:\n\"Deploying new design tokens to production EOD\"\n\nReplies (threaded, not cluttering main channel):\n- Person A: \"Looks good, tested locally\"\n- Person B: \"Breaking changes documented?\"\n- Person A: \"Yes, migration guide in PR\"\n",[6375],{"type":19,"tag":133,"props":6376,"children":6377},{"__ignoreMap":7},[6378],{"type":25,"value":6373},{"type":19,"tag":28,"props":6380,"children":6381},{},[6382],{"type":25,"value":6383},"Main channel stays clean. Interested people can see the discussion.",{"type":19,"tag":121,"props":6385,"children":6387},{"id":6386},"status-updates-not-standups",[6388],{"type":25,"value":6389},"Status Updates (Not Standups)",{"type":19,"tag":28,"props":6391,"children":6392},{},[6393],{"type":25,"value":6394},"Bad: Synchronous standup at 9am (3am for India team)",{"type":19,"tag":28,"props":6396,"children":6397},{},[6398],{"type":25,"value":6399},"Good: Async status in shared doc",{"type":19,"tag":128,"props":6401,"children":6405},{"code":6402,"language":708,"meta":7,"className":6403},"## Frontend Team Status — Week of May 15\n\n### Alice (Timeline: US Pacific)\n- **Done**: Button component refactor, 12 tests passing\n- **Doing**: Design token migration (ETA: Wed)\n- **Blocked**: Need approval from Design on color palette changes\n- **Time**: 9am-6pm PT, can overlap Tue\u002FWed\u002FThu with Europe\n\n### Bob (Timeline: Europe CET)\n- **Done**: Accessibility audit of forms, 8 WCAG violations fixed\n- **Doing**: Modal component docs\n- **Blocked**: Waiting for Alice's Button refactor to merge\n- **Time**: 9am-5pm CET, overlaps 12pm-5pm PT with Alice\n\n### Charlie (Timeline: India IST)\n- **Done**: Setup new Vue 3 project, 4 components created\n- **Doing**: Migration docs\n- **Blocked**: Need styling guidelines from Design\n- **Time**: 10am-6pm IST, overlaps Tue\u002FWed with Bob\n",[6404],"language-markdown",[6406],{"type":19,"tag":133,"props":6407,"children":6408},{"__ignoreMap":7},[6409],{"type":25,"value":6402},{"type":19,"tag":28,"props":6411,"children":6412},{},[6413],{"type":25,"value":6414},"This works for everyone, any timezone.",{"type":19,"tag":20,"props":6416,"children":6418},{"id":6417},"documentation-is-law",[6419],{"type":25,"value":6420},"Documentation Is Law",{"type":19,"tag":28,"props":6422,"children":6423},{},[6424],{"type":25,"value":6425},"Remote engineers live in documentation.",{"type":19,"tag":121,"props":6427,"children":6429},{"id":6428},"architecture-decisions",[6430],{"type":25,"value":6431},"Architecture Decisions",{"type":19,"tag":128,"props":6433,"children":6436},{"code":6434,"language":708,"meta":7,"className":6435},"## ADR-12: Design System Versioning\n\n### Context\nWe have 5 products using our design system.\nDifferent teams want to update components at different rates.\nShared state causes delays.\n\n### Decision\nUse semantic versioning with 30-day support windows.\n- Major version: Breaking changes (1 month notice)\n- Minor version: New features (backcompat)\n- Patch version: Bug fixes (auto-deploy)\n\n### Consequences\n- Positive: Teams can update independently\n- Positive: Clear upgrade path\n- Negative: Need to backport critical bugs for 30 days\n- Negative: More coordination needed\n\n### Approved by\n- Design lead (2024-03-15)\n- Frontend lead (2024-03-15)\n",[6404],[6437],{"type":19,"tag":133,"props":6438,"children":6439},{"__ignoreMap":7},[6440],{"type":25,"value":6434},{"type":19,"tag":28,"props":6442,"children":6443},{},[6444],{"type":25,"value":6445},"Store in shared repo, searchable, link from PRs.",{"type":19,"tag":121,"props":6447,"children":6449},{"id":6448},"component-documentation",[6450],{"type":25,"value":6451},"Component Documentation",{"type":19,"tag":128,"props":6453,"children":6456},{"code":6454,"language":172,"meta":7,"className":6455},"\u002F\u002F Button.vue\n\u002F**\n * Universal button component\n * \n * @component\n * @example\n * \u003CButton variant=\"primary\" size=\"lg\">Click me\u003C\u002FButton>\n * \n * @prop {string} variant - \"primary\" | \"secondary\" | \"danger\"\n * @prop {string} size - \"sm\" | \"md\" | \"lg\"\n * @prop {boolean} disabled - Disable the button\n * @prop {boolean} loading - Show loading state\n * \n * @emits click - Emitted when clicked\n * \n * @accessibility\n * - Keyboard accessible (Tab to focus, Enter\u002FSpace to activate)\n * - Aria-label for icon-only buttons\n * - Focus indicator visible (outline: 2px solid)\n * - Disabled state properly conveyed to screen readers\n *\u002F\n",[174],[6457],{"type":19,"tag":133,"props":6458,"children":6459},{"__ignoreMap":7},[6460],{"type":25,"value":6454},{"type":19,"tag":121,"props":6462,"children":6464},{"id":6463},"api-documentation",[6465],{"type":25,"value":6466},"API Documentation",{"type":19,"tag":128,"props":6468,"children":6471},{"code":6469,"language":708,"meta":7,"className":6470},"## POST \u002Fapi\u002Fusers\n\nCreate a new user.\n\n### Request\n```json\n{\n  \"name\": \"John Doe\",\n  \"email\": \"john@example.com\",\n  \"role\": \"user\" \u002F\u002F optional, defaults to \"user\"\n}\n",[6404],[6472],{"type":19,"tag":133,"props":6473,"children":6474},{"__ignoreMap":7},[6475],{"type":25,"value":6469},{"type":19,"tag":121,"props":6477,"children":6479},{"id":6478},"response",[6480],{"type":25,"value":6481},"Response",{"type":19,"tag":128,"props":6483,"children":6486},{"code":6484,"language":2179,"meta":7,"className":6485},"{\n  \"id\": \"user_123\",\n  \"name\": \"John Doe\",\n  \"email\": \"john@example.com\",\n  \"role\": \"user\",\n  \"createdAt\": \"2024-04-01T10:00:00Z\"\n}\n",[2181],[6487],{"type":19,"tag":133,"props":6488,"children":6489},{"__ignoreMap":7},[6490],{"type":25,"value":6484},{"type":19,"tag":121,"props":6492,"children":6494},{"id":6493},"errors",[6495],{"type":25,"value":6496},"Errors",{"type":19,"tag":50,"props":6498,"children":6499},{},[6500,6505,6510],{"type":19,"tag":38,"props":6501,"children":6502},{},[6503],{"type":25,"value":6504},"400: Invalid email format",{"type":19,"tag":38,"props":6506,"children":6507},{},[6508],{"type":25,"value":6509},"409: Email already exists",{"type":19,"tag":38,"props":6511,"children":6512},{},[6513],{"type":25,"value":6514},"422: Missing required fields",{"type":19,"tag":121,"props":6516,"children":6518},{"id":6517},"rate-limit",[6519],{"type":25,"value":6520},"Rate Limit",{"type":19,"tag":28,"props":6522,"children":6523},{},[6524],{"type":25,"value":6525},"100 requests per minute per API key",{"type":19,"tag":128,"props":6527,"children":6529},{"code":6528},"\n## Code Review Culture\n\nRemote code reviews must be thorough and asynchronous.\n\n### Good Code Review Comment\n\n",[6530],{"type":19,"tag":133,"props":6531,"children":6532},{"__ignoreMap":7},[6533],{"type":25,"value":6528},{"type":19,"tag":28,"props":6535,"children":6536},{},[6537],{"type":25,"value":6538},"\u002F\u002F ❌ Vague\n\"This looks off\"",{"type":19,"tag":28,"props":6540,"children":6541},{},[6542],{"type":25,"value":6543},"\u002F\u002F ✅ Specific\n\"Line 45: The loading state here doesn't clear after success.\nIf the request succeeds but then fails, isLoading is true forever.\nSuggest wrapping the entire try\u002Ffinally block:",{"type":19,"tag":28,"props":6545,"children":6546},{},[6547],{"type":25,"value":6548},"try {\nisLoading.value = true\nresponse = await api.create(data)\n\u002F\u002F handle success\n} finally {\nisLoading.value = false  \u002F\u002F Always runs\n}",{"type":19,"tag":28,"props":6550,"children":6551},{},[6552],{"type":25,"value":6553},"See similar pattern in useUser.ts line 32\"",{"type":19,"tag":128,"props":6555,"children":6557},{"code":6556},"\nInclude:\n- Line number\n- What's wrong\n- Why it's wrong\n- Suggested fix\n- Reference to similar code\n\n### Review Checklist\n\n```markdown\n## Code Review Checklist\n\n- [ ] Does the code match the PR description?\n- [ ] Is there test coverage?\n- [ ] Are there console.logs or debugging code left?\n- [ ] Does this follow our naming conventions?\n- [ ] Is the TypeScript correct (no `any`)?\n- [ ] Are error cases handled?\n- [ ] Does this need documentation?\n- [ ] Is accessibility considered?\n- [ ] Will this cause performance regressions?\n- [ ] Does this have security implications?\n",[6558],{"type":19,"tag":133,"props":6559,"children":6560},{"__ignoreMap":7},[6561],{"type":25,"value":6556},{"type":19,"tag":28,"props":6563,"children":6564},{},[6565],{"type":25,"value":6566},"Use it for every PR.",{"type":19,"tag":20,"props":6568,"children":6570},{"id":6569},"timezone-management",[6571],{"type":25,"value":6572},"Timezone Management",{"type":19,"tag":28,"props":6574,"children":6575},{},[6576],{"type":25,"value":6577},"Working across timezones requires structure:",{"type":19,"tag":121,"props":6579,"children":6581},{"id":6580},"core-collaboration-hours",[6582],{"type":25,"value":6583},"Core Collaboration Hours",{"type":19,"tag":28,"props":6585,"children":6586},{},[6587],{"type":25,"value":6588},"Define overlapping times when synchronous work happens:",{"type":19,"tag":128,"props":6590,"children":6592},{"code":6591},"Team spans: IST (UTC+5:30), CET (UTC+1), PST (UTC-8)\n\nOverlap windows:\n- IST + CET: 10am-1pm IST = 4:30am-7:30am CET ❌ (too early)\n- CET + PST: 12pm-5pm CET = 3am-8am PST ❌ (too early)\n- IST + PST: No overlap ❌\n\nSolution: Async-first, with scheduled 1-on-1s\n\nMonday: IST + CET overlap (2-hour sync)\nWednesday: CET + PST overlap (2-hour sync)\nFriday: Full team async, no meetings\n",[6593],{"type":19,"tag":133,"props":6594,"children":6595},{"__ignoreMap":7},[6596],{"type":25,"value":6591},{"type":19,"tag":121,"props":6598,"children":6600},{"id":6599},"meeting-best-practices",[6601],{"type":25,"value":6602},"Meeting Best Practices",{"type":19,"tag":128,"props":6604,"children":6606},{"code":6605},"❌ 9am PT (5pm CET, 1:30am IST)\n  - Impossible for India team\n\n✅ 3pm PT (12am CET, 10:30pm IST)\n  - Morning for EU, evening for India, working hours for US\n\n❌ Monthly recurring at same time\n  - Someone is always inconvenienced\n\n✅ Rotate meeting times monthly\n  - Different teams take the inconvenient slot each month\n  - Everyone shares the burden\n",[6607],{"type":19,"tag":133,"props":6608,"children":6609},{"__ignoreMap":7},[6610],{"type":25,"value":6605},{"type":19,"tag":121,"props":6612,"children":6614},{"id":6613},"communication-norms",[6615],{"type":25,"value":6616},"Communication Norms",{"type":19,"tag":128,"props":6618,"children":6621},{"code":6619,"language":708,"meta":7,"className":6620},"## Communication SLA\n\n### Chat (Slack)\n- Team questions: 24-hour response\n- Feature requests: 48-hour response\n- Blocking issues: 2-hour response if during working hours\n\n### PRs\n- Approval: 24 hours\n- Blocking feedback: 48 hours\n\n### Urgent\n- Use @channel sparingly (maybe once\u002Fmonth)\n- Use if production is down or major deadline at risk\n- Include: What's broken, who's addressing it, ETA\n",[6404],[6622],{"type":19,"tag":133,"props":6623,"children":6624},{"__ignoreMap":7},[6625],{"type":25,"value":6619},{"type":19,"tag":20,"props":6627,"children":6629},{"id":6628},"tools-that-work-remote",[6630],{"type":25,"value":6631},"Tools That Work Remote",{"type":19,"tag":128,"props":6633,"children":6635},{"code":6634},"✅ Async Decision Making\n- GitHub issues for RFCs (Requests for Comments)\n- Decision deadline in issue\n- Link from PRs\n\n✅ Asynchronous Testing\n- CI\u002FCD that runs before merge\n- Shared staging environment (no manual testing)\n- Performance benchmarks automated\n\n✅ Knowledge Sharing\n- Video recordings of tech talks (async-watchable)\n- Loom videos for complex explanations\n- Recorded demos of features\n\n✅ Code Ownership\n- CODEOWNERS file in repo\n- Auto-assigned reviewers\n- Clear escalation path\n",[6636],{"type":19,"tag":133,"props":6637,"children":6638},{"__ignoreMap":7},[6639],{"type":25,"value":6634},{"type":19,"tag":20,"props":6641,"children":6643},{"id":6642},"remote-interview-tips-meta",[6644],{"type":25,"value":6645},"Remote Interview Tips (Meta)",{"type":19,"tag":28,"props":6647,"children":6648},{},[6649],{"type":25,"value":6650},"You're hiring a remote engineer. Watch for:",{"type":19,"tag":34,"props":6652,"children":6653},{},[6654,6664,6674,6684,6694],{"type":19,"tag":38,"props":6655,"children":6656},{},[6657,6662],{"type":19,"tag":42,"props":6658,"children":6659},{},[6660],{"type":25,"value":6661},"Written communication",{"type":25,"value":6663}," — can they explain clearly?",{"type":19,"tag":38,"props":6665,"children":6666},{},[6667,6672],{"type":19,"tag":42,"props":6668,"children":6669},{},[6670],{"type":25,"value":6671},"Self-motivation",{"type":25,"value":6673}," — do they ask good questions during prep?",{"type":19,"tag":38,"props":6675,"children":6676},{},[6677,6682],{"type":19,"tag":42,"props":6678,"children":6679},{},[6680],{"type":25,"value":6681},"Timezone awareness",{"type":25,"value":6683}," — do they mention working across timezones?",{"type":19,"tag":38,"props":6685,"children":6686},{},[6687,6692],{"type":19,"tag":42,"props":6688,"children":6689},{},[6690],{"type":25,"value":6691},"Documentation review",{"type":25,"value":6693}," — did they read our docs before interview?",{"type":19,"tag":38,"props":6695,"children":6696},{},[6697,6702],{"type":19,"tag":42,"props":6698,"children":6699},{},[6700],{"type":25,"value":6701},"Async examples",{"type":25,"value":6703}," — how have they worked async before?",{"type":19,"tag":20,"props":6705,"children":6707},{"id":6706},"results-of-good-remote-practices",[6708],{"type":25,"value":6709},"Results of Good Remote Practices",{"type":19,"tag":28,"props":6711,"children":6712},{},[6713],{"type":25,"value":6714},"After adopting these practices:",{"type":19,"tag":50,"props":6716,"children":6717},{},[6718,6728,6737,6747],{"type":19,"tag":38,"props":6719,"children":6720},{},[6721,6726],{"type":19,"tag":42,"props":6722,"children":6723},{},[6724],{"type":25,"value":6725},"Decision speed",{"type":25,"value":6727},": 2 days → 24 hours (async docs > meetings)",{"type":19,"tag":38,"props":6729,"children":6730},{},[6731,6735],{"type":19,"tag":42,"props":6732,"children":6733},{},[6734],{"type":25,"value":3136},{"type":25,"value":6736},": 4 weeks → 1.5 weeks (docs are source of truth)",{"type":19,"tag":38,"props":6738,"children":6739},{},[6740,6745],{"type":19,"tag":42,"props":6741,"children":6742},{},[6743],{"type":25,"value":6744},"Bug reports",{"type":25,"value":6746},": 4\u002Fweek → 1\u002Fweek (context is clear)",{"type":19,"tag":38,"props":6748,"children":6749},{},[6750,6755],{"type":19,"tag":42,"props":6751,"children":6752},{},[6753],{"type":25,"value":6754},"Developer satisfaction",{"type":25,"value":6756},": 7\u002F10 → 9\u002F10 (no sync meeting fatigue)",{"type":19,"tag":20,"props":6758,"children":6760},{"id":6759},"the-secret",[6761],{"type":25,"value":6762},"The Secret",{"type":19,"tag":28,"props":6764,"children":6765},{},[6766,6768],{"type":25,"value":6767},"Remote work isn't about Zoom calls. It's about ",{"type":19,"tag":42,"props":6769,"children":6770},{},[6771],{"type":25,"value":6772},"clear documentation, trust, and autonomy.",{"type":19,"tag":28,"props":6774,"children":6775},{},[6776],{"type":25,"value":6777},"Engineers who can work async:",{"type":19,"tag":50,"props":6779,"children":6780},{},[6781,6786,6791,6796],{"type":19,"tag":38,"props":6782,"children":6783},{},[6784],{"type":25,"value":6785},"Ship faster",{"type":19,"tag":38,"props":6787,"children":6788},{},[6789],{"type":25,"value":6790},"Need less supervision",{"type":19,"tag":38,"props":6792,"children":6793},{},[6794],{"type":25,"value":6795},"Solve problems independently",{"type":19,"tag":38,"props":6797,"children":6798},{},[6799],{"type":25,"value":6800},"Get better sleep (no midnight calls)",{"type":19,"tag":28,"props":6802,"children":6803},{},[6804],{"type":25,"value":6805},"The best remote engineers aren't chatty. They're clear writers.",{"title":7,"searchDepth":681,"depth":681,"links":6807},[6808,6809,6814,6822,6827,6828,6829,6830],{"id":6218,"depth":681,"text":6221},{"id":6266,"depth":681,"text":6269,"children":6810},[6811,6812,6813],{"id":6272,"depth":687,"text":6275},{"id":6362,"depth":687,"text":6365},{"id":6386,"depth":687,"text":6389},{"id":6417,"depth":681,"text":6420,"children":6815},[6816,6817,6818,6819,6820,6821],{"id":6428,"depth":687,"text":6431},{"id":6448,"depth":687,"text":6451},{"id":6463,"depth":687,"text":6466},{"id":6478,"depth":687,"text":6481},{"id":6493,"depth":687,"text":6496},{"id":6517,"depth":687,"text":6520},{"id":6569,"depth":681,"text":6572,"children":6823},[6824,6825,6826],{"id":6580,"depth":687,"text":6583},{"id":6599,"depth":687,"text":6602},{"id":6613,"depth":687,"text":6616},{"id":6628,"depth":681,"text":6631},{"id":6642,"depth":681,"text":6645},{"id":6706,"depth":681,"text":6709},{"id":6759,"depth":681,"text":6762},"content:blog:remote-frontend-engineering-best-practices.md","blog\u002Fremote-frontend-engineering-best-practices.md","blog\u002Fremote-frontend-engineering-best-practices",{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"readingTime":11,"tags":6835,"featured":6,"body":6836,"_type":708,"_id":709,"_source":710,"_file":711,"_stem":712,"_extension":713},[13,14],{"type":16,"children":6837,"toc":7355},[6838,6842,6846,6910,6914,6918,6925,6929,6933,6937,6945,6949,6957,6961,6969,6973,6977,6981,6985,6993,6997,7012,7016,7024,7028,7032,7040,7044,7048,7056,7060,7064,7068,7076,7080,7088,7092,7100,7104,7108,7148,7152,7160,7164,7172,7176,7184,7188,7192,7276,7280,7284,7292,7296,7304,7308,7351],{"type":19,"tag":20,"props":6839,"children":6840},{"id":22},[6841],{"type":25,"value":26},{"type":19,"tag":28,"props":6843,"children":6844},{},[6845],{"type":25,"value":32},{"type":19,"tag":34,"props":6847,"children":6848},{},[6849,6868,6891],{"type":19,"tag":38,"props":6850,"children":6851},{},[6852,6856,6857],{"type":19,"tag":42,"props":6853,"children":6854},{},[6855],{"type":25,"value":46},{"type":25,"value":48},{"type":19,"tag":50,"props":6858,"children":6859},{},[6860,6864],{"type":19,"tag":38,"props":6861,"children":6862},{},[6863],{"type":25,"value":57},{"type":19,"tag":38,"props":6865,"children":6866},{},[6867],{"type":25,"value":62},{"type":19,"tag":38,"props":6869,"children":6870},{},[6871,6875,6876],{"type":19,"tag":42,"props":6872,"children":6873},{},[6874],{"type":25,"value":70},{"type":25,"value":72},{"type":19,"tag":50,"props":6877,"children":6878},{},[6879,6883,6887],{"type":19,"tag":38,"props":6880,"children":6881},{},[6882],{"type":25,"value":80},{"type":19,"tag":38,"props":6884,"children":6885},{},[6886],{"type":25,"value":85},{"type":19,"tag":38,"props":6888,"children":6889},{},[6890],{"type":25,"value":90},{"type":19,"tag":38,"props":6892,"children":6893},{},[6894,6898,6899],{"type":19,"tag":42,"props":6895,"children":6896},{},[6897],{"type":25,"value":98},{"type":25,"value":100},{"type":19,"tag":50,"props":6900,"children":6901},{},[6902,6906],{"type":19,"tag":38,"props":6903,"children":6904},{},[6905],{"type":25,"value":108},{"type":19,"tag":38,"props":6907,"children":6908},{},[6909],{"type":25,"value":113},{"type":19,"tag":20,"props":6911,"children":6912},{"id":116},[6913],{"type":25,"value":119},{"type":19,"tag":121,"props":6915,"children":6916},{"id":123},[6917],{"type":25,"value":126},{"type":19,"tag":128,"props":6919,"children":6920},{"code":130},[6921],{"type":19,"tag":133,"props":6922,"children":6923},{"__ignoreMap":7},[6924],{"type":25,"value":130},{"type":19,"tag":28,"props":6926,"children":6927},{},[6928],{"type":25,"value":141},{"type":19,"tag":121,"props":6930,"children":6931},{"id":144},[6932],{"type":25,"value":147},{"type":19,"tag":28,"props":6934,"children":6935},{},[6936],{"type":25,"value":152},{"type":19,"tag":128,"props":6938,"children":6940},{"code":155,"language":156,"meta":7,"className":6939},[158],[6941],{"type":19,"tag":133,"props":6942,"children":6943},{"__ignoreMap":7},[6944],{"type":25,"value":155},{"type":19,"tag":28,"props":6946,"children":6947},{},[6948],{"type":25,"value":168},{"type":19,"tag":128,"props":6950,"children":6952},{"code":171,"language":172,"meta":7,"className":6951},[174],[6953],{"type":19,"tag":133,"props":6954,"children":6955},{"__ignoreMap":7},[6956],{"type":25,"value":171},{"type":19,"tag":28,"props":6958,"children":6959},{},[6960],{"type":25,"value":184},{"type":19,"tag":128,"props":6962,"children":6964},{"code":187,"language":172,"meta":7,"className":6963},[174],[6965],{"type":19,"tag":133,"props":6966,"children":6967},{"__ignoreMap":7},[6968],{"type":25,"value":187},{"type":19,"tag":20,"props":6970,"children":6971},{"id":196},[6972],{"type":25,"value":199},{"type":19,"tag":28,"props":6974,"children":6975},{},[6976],{"type":25,"value":204},{"type":19,"tag":121,"props":6978,"children":6979},{"id":207},[6980],{"type":25,"value":210},{"type":19,"tag":28,"props":6982,"children":6983},{},[6984],{"type":25,"value":215},{"type":19,"tag":128,"props":6986,"children":6988},{"code":218,"language":172,"meta":7,"className":6987},[174],[6989],{"type":19,"tag":133,"props":6990,"children":6991},{"__ignoreMap":7},[6992],{"type":25,"value":218},{"type":19,"tag":28,"props":6994,"children":6995},{},[6996],{"type":25,"value":229},{"type":19,"tag":50,"props":6998,"children":6999},{},[7000,7004,7008],{"type":19,"tag":38,"props":7001,"children":7002},{},[7003],{"type":25,"value":237},{"type":19,"tag":38,"props":7005,"children":7006},{},[7007],{"type":25,"value":242},{"type":19,"tag":38,"props":7009,"children":7010},{},[7011],{"type":25,"value":247},{"type":19,"tag":121,"props":7013,"children":7014},{"id":250},[7015],{"type":25,"value":253},{"type":19,"tag":128,"props":7017,"children":7019},{"code":256,"language":172,"meta":7,"className":7018},[174],[7020],{"type":19,"tag":133,"props":7021,"children":7022},{"__ignoreMap":7},[7023],{"type":25,"value":256},{"type":19,"tag":121,"props":7025,"children":7026},{"id":265},[7027],{"type":25,"value":268},{"type":19,"tag":28,"props":7029,"children":7030},{},[7031],{"type":25,"value":273},{"type":19,"tag":128,"props":7033,"children":7035},{"code":276,"language":172,"meta":7,"className":7034},[174],[7036],{"type":19,"tag":133,"props":7037,"children":7038},{"__ignoreMap":7},[7039],{"type":25,"value":276},{"type":19,"tag":121,"props":7041,"children":7042},{"id":285},[7043],{"type":25,"value":288},{"type":19,"tag":28,"props":7045,"children":7046},{},[7047],{"type":25,"value":293},{"type":19,"tag":128,"props":7049,"children":7051},{"code":296,"language":297,"meta":7,"className":7050},[299],[7052],{"type":19,"tag":133,"props":7053,"children":7054},{"__ignoreMap":7},[7055],{"type":25,"value":296},{"type":19,"tag":20,"props":7057,"children":7058},{"id":307},[7059],{"type":25,"value":310},{"type":19,"tag":28,"props":7061,"children":7062},{},[7063],{"type":25,"value":315},{"type":19,"tag":121,"props":7065,"children":7066},{"id":318},[7067],{"type":25,"value":321},{"type":19,"tag":128,"props":7069,"children":7071},{"code":324,"language":172,"meta":7,"className":7070},[174],[7072],{"type":19,"tag":133,"props":7073,"children":7074},{"__ignoreMap":7},[7075],{"type":25,"value":324},{"type":19,"tag":121,"props":7077,"children":7078},{"id":333},[7079],{"type":25,"value":336},{"type":19,"tag":128,"props":7081,"children":7083},{"code":339,"language":172,"meta":7,"className":7082},[174],[7084],{"type":19,"tag":133,"props":7085,"children":7086},{"__ignoreMap":7},[7087],{"type":25,"value":339},{"type":19,"tag":121,"props":7089,"children":7090},{"id":348},[7091],{"type":25,"value":351},{"type":19,"tag":128,"props":7093,"children":7095},{"code":354,"language":297,"meta":7,"className":7094},[299],[7096],{"type":19,"tag":133,"props":7097,"children":7098},{"__ignoreMap":7},[7099],{"type":25,"value":354},{"type":19,"tag":20,"props":7101,"children":7102},{"id":363},[7103],{"type":25,"value":366},{"type":19,"tag":28,"props":7105,"children":7106},{},[7107],{"type":25,"value":371},{"type":19,"tag":34,"props":7109,"children":7110},{},[7111,7119,7127,7140],{"type":19,"tag":38,"props":7112,"children":7113},{},[7114,7118],{"type":19,"tag":42,"props":7115,"children":7116},{},[7117],{"type":25,"value":382},{"type":25,"value":384},{"type":19,"tag":38,"props":7120,"children":7121},{},[7122,7126],{"type":19,"tag":42,"props":7123,"children":7124},{},[7125],{"type":25,"value":392},{"type":25,"value":394},{"type":19,"tag":38,"props":7128,"children":7129},{},[7130,7134,7135],{"type":19,"tag":42,"props":7131,"children":7132},{},[7133],{"type":25,"value":402},{"type":25,"value":404},{"type":19,"tag":133,"props":7136,"children":7138},{"className":7137},[],[7139],{"type":25,"value":410},{"type":19,"tag":38,"props":7141,"children":7142},{},[7143,7147],{"type":19,"tag":42,"props":7144,"children":7145},{},[7146],{"type":25,"value":418},{"type":25,"value":420},{"type":19,"tag":121,"props":7149,"children":7150},{"id":423},[7151],{"type":25,"value":426},{"type":19,"tag":128,"props":7153,"children":7155},{"code":429,"language":297,"meta":7,"className":7154},[299],[7156],{"type":19,"tag":133,"props":7157,"children":7158},{"__ignoreMap":7},[7159],{"type":25,"value":429},{"type":19,"tag":121,"props":7161,"children":7162},{"id":438},[7163],{"type":25,"value":441},{"type":19,"tag":128,"props":7165,"children":7167},{"code":444,"language":172,"meta":7,"className":7166},[174],[7168],{"type":19,"tag":133,"props":7169,"children":7170},{"__ignoreMap":7},[7171],{"type":25,"value":444},{"type":19,"tag":121,"props":7173,"children":7174},{"id":453},[7175],{"type":25,"value":456},{"type":19,"tag":128,"props":7177,"children":7179},{"code":459,"language":297,"meta":7,"className":7178},[299],[7180],{"type":19,"tag":133,"props":7181,"children":7182},{"__ignoreMap":7},[7183],{"type":25,"value":459},{"type":19,"tag":20,"props":7185,"children":7186},{"id":468},[7187],{"type":25,"value":471},{"type":19,"tag":28,"props":7189,"children":7190},{},[7191],{"type":25,"value":476},{"type":19,"tag":478,"props":7193,"children":7194},{},[7195,7213],{"type":19,"tag":482,"props":7196,"children":7197},{},[7198],{"type":19,"tag":486,"props":7199,"children":7200},{},[7201,7205,7209],{"type":19,"tag":490,"props":7202,"children":7203},{},[7204],{"type":25,"value":494},{"type":19,"tag":490,"props":7206,"children":7207},{},[7208],{"type":25,"value":499},{"type":19,"tag":490,"props":7210,"children":7211},{},[7212],{"type":25,"value":504},{"type":19,"tag":506,"props":7214,"children":7215},{},[7216,7231,7246,7261],{"type":19,"tag":486,"props":7217,"children":7218},{},[7219,7223,7227],{"type":19,"tag":513,"props":7220,"children":7221},{},[7222],{"type":25,"value":517},{"type":19,"tag":513,"props":7224,"children":7225},{},[7226],{"type":25,"value":522},{"type":19,"tag":513,"props":7228,"children":7229},{},[7230],{"type":25,"value":527},{"type":19,"tag":486,"props":7232,"children":7233},{},[7234,7238,7242],{"type":19,"tag":513,"props":7235,"children":7236},{},[7237],{"type":25,"value":535},{"type":19,"tag":513,"props":7239,"children":7240},{},[7241],{"type":25,"value":540},{"type":19,"tag":513,"props":7243,"children":7244},{},[7245],{"type":25,"value":545},{"type":19,"tag":486,"props":7247,"children":7248},{},[7249,7253,7257],{"type":19,"tag":513,"props":7250,"children":7251},{},[7252],{"type":25,"value":553},{"type":19,"tag":513,"props":7254,"children":7255},{},[7256],{"type":25,"value":558},{"type":19,"tag":513,"props":7258,"children":7259},{},[7260],{"type":25,"value":563},{"type":19,"tag":486,"props":7262,"children":7263},{},[7264,7268,7272],{"type":19,"tag":513,"props":7265,"children":7266},{},[7267],{"type":25,"value":571},{"type":19,"tag":513,"props":7269,"children":7270},{},[7271],{"type":25,"value":576},{"type":19,"tag":513,"props":7273,"children":7274},{},[7275],{"type":25,"value":581},{"type":19,"tag":20,"props":7277,"children":7278},{"id":584},[7279],{"type":25,"value":587},{"type":19,"tag":28,"props":7281,"children":7282},{},[7283],{"type":25,"value":592},{"type":19,"tag":128,"props":7285,"children":7287},{"code":595,"language":172,"meta":7,"className":7286},[174],[7288],{"type":19,"tag":133,"props":7289,"children":7290},{"__ignoreMap":7},[7291],{"type":25,"value":595},{"type":19,"tag":28,"props":7293,"children":7294},{},[7295],{"type":25,"value":606},{"type":19,"tag":128,"props":7297,"children":7299},{"code":609,"language":156,"meta":7,"className":7298},[158],[7300],{"type":19,"tag":133,"props":7301,"children":7302},{"__ignoreMap":7},[7303],{"type":25,"value":609},{"type":19,"tag":20,"props":7305,"children":7306},{"id":618},[7307],{"type":25,"value":621},{"type":19,"tag":34,"props":7309,"children":7310},{},[7311,7319,7327,7335,7343],{"type":19,"tag":38,"props":7312,"children":7313},{},[7314,7318],{"type":19,"tag":42,"props":7315,"children":7316},{},[7317],{"type":25,"value":632},{"type":25,"value":634},{"type":19,"tag":38,"props":7320,"children":7321},{},[7322,7326],{"type":19,"tag":42,"props":7323,"children":7324},{},[7325],{"type":25,"value":642},{"type":25,"value":644},{"type":19,"tag":38,"props":7328,"children":7329},{},[7330,7334],{"type":19,"tag":42,"props":7331,"children":7332},{},[7333],{"type":25,"value":652},{"type":25,"value":654},{"type":19,"tag":38,"props":7336,"children":7337},{},[7338,7342],{"type":19,"tag":42,"props":7339,"children":7340},{},[7341],{"type":25,"value":662},{"type":25,"value":664},{"type":19,"tag":38,"props":7344,"children":7345},{},[7346,7350],{"type":19,"tag":42,"props":7347,"children":7348},{},[7349],{"type":25,"value":672},{"type":25,"value":674},{"type":19,"tag":28,"props":7352,"children":7353},{},[7354],{"type":25,"value":679},{"title":7,"searchDepth":681,"depth":681,"links":7356},[7357,7358,7362,7368,7373,7378,7379,7380],{"id":22,"depth":681,"text":26},{"id":116,"depth":681,"text":119,"children":7359},[7360,7361],{"id":123,"depth":687,"text":126},{"id":144,"depth":687,"text":147},{"id":196,"depth":681,"text":199,"children":7363},[7364,7365,7366,7367],{"id":207,"depth":687,"text":210},{"id":250,"depth":687,"text":253},{"id":265,"depth":687,"text":268},{"id":285,"depth":687,"text":288},{"id":307,"depth":681,"text":310,"children":7369},[7370,7371,7372],{"id":318,"depth":687,"text":321},{"id":333,"depth":687,"text":336},{"id":348,"depth":687,"text":351},{"id":363,"depth":681,"text":366,"children":7374},[7375,7376,7377],{"id":423,"depth":687,"text":426},{"id":438,"depth":687,"text":441},{"id":453,"depth":687,"text":456},{"id":468,"depth":681,"text":471},{"id":584,"depth":681,"text":587},{"id":618,"depth":681,"text":621},1783235260544]