[{"data":1,"prerenderedAt":7135},["ShallowReactive",2],{"blog-post-vue-2-to-vue-3-migration-at-scale":3,"blog-posts":570,"content-query-YnIXwcq26N":6711},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"updated":11,"readingTime":12,"tags":13,"featured":17,"body":18,"_type":564,"_id":565,"_source":566,"_file":567,"_stem":568,"_extension":569},"\u002Fblog\u002Fvue-2-to-vue-3-migration-at-scale","blog",false,"","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,[14,15,16],"Vue","Architecture","Performance",true,{"type":19,"children":20,"toc":550},"root",[21,30,36,41,95,100,106,113,132,144,149,167,173,178,212,219,228,233,282,288,301,307,318,323,329,338,343,349,358,363,369,374,383,388,393,399,404,412,437,443,496,502,545],{"type":22,"tag":23,"props":24,"children":26},"element","h2",{"id":25},"the-challenge-why-we-needed-to-migrate",[27],{"type":28,"value":29},"text","The Challenge: Why We Needed to Migrate",{"type":22,"tag":31,"props":32,"children":33},"p",{},[34],{"type":28,"value":35},"We faced a critical decision when Vue 3 became stable in 2021. Our Vue 2 codebase had grown large across multiple teams.",{"type":22,"tag":31,"props":37,"children":38},{},[39],{"type":28,"value":40},"The pain points were real:",{"type":22,"tag":42,"props":43,"children":44},"ul",{},[45,57,67,76],{"type":22,"tag":46,"props":47,"children":48},"li",{},[49,55],{"type":22,"tag":50,"props":51,"children":52},"strong",{},[53],{"type":28,"value":54},"Type Safety",{"type":28,"value":56},": Vue 2 + TypeScript was never fully integrated. The IDE caught some issues; the rest surfaced in QA",{"type":22,"tag":46,"props":58,"children":59},{},[60,65],{"type":22,"tag":50,"props":61,"children":62},{},[63],{"type":28,"value":64},"Composition Reuse",{"type":28,"value":66},": Mixins led to naming collisions and unclear data flow. Multiple teams independently \"solved\" the same problems",{"type":22,"tag":46,"props":68,"children":69},{},[70,74],{"type":22,"tag":50,"props":71,"children":72},{},[73],{"type":28,"value":16},{"type":28,"value":75},": Bundle size kept growing. Tree-shaking didn't work well with Vue 2's runtime structure",{"type":22,"tag":46,"props":77,"children":78},{},[79,84,86,93],{"type":22,"tag":50,"props":80,"children":81},{},[82],{"type":28,"value":83},"Developer Experience",{"type":28,"value":85},": New team members struggled with implicit ",{"type":22,"tag":87,"props":88,"children":90},"code",{"className":89},[],[91],{"type":28,"value":92},"this",{"type":28,"value":94}," context and composition patterns",{"type":22,"tag":31,"props":96,"children":97},{},[98],{"type":28,"value":99},"The business case was compelling: fewer bugs in production, faster onboarding, a smaller bundle.",{"type":22,"tag":23,"props":101,"children":103},{"id":102},"our-migration-strategy",[104],{"type":28,"value":105},"Our Migration Strategy",{"type":22,"tag":107,"props":108,"children":110},"h3",{"id":109},"phase-1-set-up-build-tooling-1-week",[111],{"type":28,"value":112},"Phase 1: Set Up Build Tooling (1 week)",{"type":22,"tag":31,"props":114,"children":115},{},[116,118,123,125,130],{"type":28,"value":117},"We chose ",{"type":22,"tag":50,"props":119,"children":120},{},[121],{"type":28,"value":122},"Vite",{"type":28,"value":124}," + ",{"type":22,"tag":50,"props":126,"children":127},{},[128],{"type":28,"value":129},"@vitejs\u002Fplugin-vue",{"type":28,"value":131}," instead of webpack. This decision paid dividends:",{"type":22,"tag":133,"props":134,"children":139},"pre",{"className":135,"code":137,"language":138,"meta":7},[136],"language-typescript","\u002F\u002F vite.config.ts\nexport default {\n  plugins: [vue(), visualizer()],\n  build: {\n    rollupOptions: {\n      output: { manualChunks: customChunks }\n    }\n  }\n}\n","typescript",[140],{"type":22,"tag":87,"props":141,"children":142},{"__ignoreMap":7},[143],{"type":28,"value":137},{"type":22,"tag":31,"props":145,"children":146},{},[147],{"type":28,"value":148},"Bundle analysis immediately showed why the tooling move pays for itself:",{"type":22,"tag":42,"props":150,"children":151},{},[152,157,162],{"type":22,"tag":46,"props":153,"children":154},{},[155],{"type":28,"value":156},"Vite's default code splitting cut the main bundle substantially before we touched a component",{"type":22,"tag":46,"props":158,"children":159},{},[160],{"type":28,"value":161},"Build times dropped from tens of seconds under webpack to single digits under Vite",{"type":22,"tag":46,"props":163,"children":164},{},[165],{"type":28,"value":166},"Hot Module Replacement went from a coffee-sip wait to effectively instant",{"type":22,"tag":107,"props":168,"children":170},{"id":169},"phase-2-incremental-component-migration-6-weeks",[171],{"type":28,"value":172},"Phase 2: Incremental Component Migration (6 weeks)",{"type":22,"tag":31,"props":174,"children":175},{},[176],{"type":28,"value":177},"We didn't rewrite everything. Instead, we:",{"type":22,"tag":179,"props":180,"children":181},"ol",{},[182,192,202],{"type":22,"tag":46,"props":183,"children":184},{},[185,190],{"type":22,"tag":50,"props":186,"children":187},{},[188],{"type":28,"value":189},"Identified high-ROI components",{"type":28,"value":191}," — components used across multiple products, or frequently modified",{"type":22,"tag":46,"props":193,"children":194},{},[195,200],{"type":22,"tag":50,"props":196,"children":197},{},[198],{"type":28,"value":199},"Created a co-existence layer",{"type":28,"value":201}," — Vue 2 and Vue 3 components lived side-by-side",{"type":22,"tag":46,"props":203,"children":204},{},[205,210],{"type":22,"tag":50,"props":206,"children":207},{},[208],{"type":28,"value":209},"Migrated by priority",{"type":28,"value":211}," — a steady per-engineer cadence, not a big-bang rewrite",{"type":22,"tag":213,"props":214,"children":216},"h4",{"id":215},"key-pattern-the-composition-api-advantage",[217],{"type":28,"value":218},"Key Pattern: The Composition API Advantage",{"type":22,"tag":133,"props":220,"children":223},{"className":221,"code":222,"language":138,"meta":7},[136],"\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",[224],{"type":22,"tag":87,"props":225,"children":226},{"__ignoreMap":7},[227],{"type":28,"value":222},{"type":22,"tag":31,"props":229,"children":230},{},[231],{"type":28,"value":232},"The Composition API version:",{"type":22,"tag":42,"props":234,"children":235},{},[236,246,256,266],{"type":22,"tag":46,"props":237,"children":238},{},[239,244],{"type":22,"tag":50,"props":240,"children":241},{},[242],{"type":28,"value":243},"Eliminates naming collisions",{"type":28,"value":245}," — composables are just functions, no implicit merging",{"type":22,"tag":46,"props":247,"children":248},{},[249,254],{"type":22,"tag":50,"props":250,"children":251},{},[252],{"type":28,"value":253},"Makes data flow explicit",{"type":28,"value":255}," — you see exactly which data is used",{"type":22,"tag":46,"props":257,"children":258},{},[259,264],{"type":22,"tag":50,"props":260,"children":261},{},[262],{"type":28,"value":263},"Enables better tree-shaking",{"type":28,"value":265}," — unused composables are easier to identify",{"type":22,"tag":46,"props":267,"children":268},{},[269,274,276],{"type":22,"tag":50,"props":270,"children":271},{},[272],{"type":28,"value":273},"Improves TypeScript",{"type":28,"value":275}," — full type inference without ",{"type":22,"tag":87,"props":277,"children":279},{"className":278},[],[280],{"type":28,"value":281},"as any",{"type":22,"tag":107,"props":283,"children":285},{"id":284},"phase-3-handle-the-breaking-changes",[286],{"type":28,"value":287},"Phase 3: Handle the Breaking Changes",{"type":22,"tag":31,"props":289,"children":290},{},[291,293,299],{"type":28,"value":292},"Vue 3's breaking changes were documented, but the ",{"type":22,"tag":294,"props":295,"children":296},"em",{},[297],{"type":28,"value":298},"scale",{"type":28,"value":300}," matters. Here's what actually cost us the most time:",{"type":22,"tag":213,"props":302,"children":304},{"id":303},"_1-event-handling-3-days-spent",[305],{"type":28,"value":306},"1. Event Handling (3 days spent)",{"type":22,"tag":133,"props":308,"children":313},{"className":309,"code":311,"language":312,"meta":7},[310],"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",[314],{"type":22,"tag":87,"props":315,"children":316},{"__ignoreMap":7},[317],{"type":28,"value":311},{"type":22,"tag":31,"props":319,"children":320},{},[321],{"type":28,"value":322},"We had instances of this all over the codebase. Automated search + manual review took longer than the code changes themselves.",{"type":22,"tag":213,"props":324,"children":326},{"id":325},"_2-async-components",[327],{"type":28,"value":328},"2. Async Components",{"type":22,"tag":133,"props":330,"children":333},{"className":331,"code":332,"language":312,"meta":7},[310],"\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",[334],{"type":22,"tag":87,"props":335,"children":336},{"__ignoreMap":7},[337],{"type":28,"value":332},{"type":22,"tag":31,"props":339,"children":340},{},[341],{"type":28,"value":342},"Dozens more locations to update. Pro tip: codemods exist for this, but they're not perfect. Invest time in understanding edge cases.",{"type":22,"tag":213,"props":344,"children":346},{"id":345},"_3-filters-computed-properties",[347],{"type":28,"value":348},"3. Filters → Computed Properties",{"type":22,"tag":133,"props":350,"children":353},{"className":351,"code":352,"language":312,"meta":7},[310],"\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",[354],{"type":22,"tag":87,"props":355,"children":356},{"__ignoreMap":7},[357],{"type":28,"value":352},{"type":22,"tag":31,"props":359,"children":360},{},[361],{"type":28,"value":362},"Dozens of filter usages. We created a utility module to centralize custom filters as functions.",{"type":22,"tag":107,"props":364,"children":366},{"id":365},"phase-4-test-migration-parallel-4-weeks",[367],{"type":28,"value":368},"Phase 4: Test Migration (Parallel, 4 weeks)",{"type":22,"tag":31,"props":370,"children":371},{},[372],{"type":28,"value":373},"Our Jest test suite had to be updated to work with Vue 3. The good news: tests were actually easier to write.",{"type":22,"tag":133,"props":375,"children":378},{"className":376,"code":377,"language":138,"meta":7},[136],"\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",[379],{"type":22,"tag":87,"props":380,"children":381},{"__ignoreMap":7},[382],{"type":28,"value":377},{"type":22,"tag":31,"props":384,"children":385},{},[386],{"type":28,"value":387},"We migrated the full test suite.",{"type":22,"tag":31,"props":389,"children":390},{},[391],{"type":28,"value":392},"It took weeks, not days — but many tests revealed brittleness in the original code. This was actually valuable.",{"type":22,"tag":23,"props":394,"children":396},{"id":395},"the-results",[397],{"type":28,"value":398},"The Results",{"type":22,"tag":31,"props":400,"children":401},{},[402],{"type":28,"value":403},"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":22,"tag":31,"props":405,"children":406},{},[407],{"type":22,"tag":50,"props":408,"children":409},{},[410],{"type":28,"value":411},"What held up qualitatively:",{"type":22,"tag":42,"props":413,"children":414},{},[415,420,432],{"type":22,"tag":46,"props":416,"children":417},{},[418],{"type":28,"value":419},"Tree-shaking finally worked, so dead code stopped shipping",{"type":22,"tag":46,"props":421,"children":422},{},[423,425,430],{"type":28,"value":424},"Full TypeScript inference replaced ",{"type":22,"tag":87,"props":426,"children":428},{"className":427},[],[429],{"type":28,"value":281},{"type":28,"value":431}," scattered through the codebase",{"type":22,"tag":46,"props":433,"children":434},{},[435],{"type":28,"value":436},"Vite's dev server made the edit-refresh loop feel free again",{"type":22,"tag":23,"props":438,"children":440},{"id":439},"lessons-for-your-codebase",[441],{"type":28,"value":442},"Lessons for Your Codebase",{"type":22,"tag":179,"props":444,"children":445},{},[446,456,466,476,486],{"type":22,"tag":46,"props":447,"children":448},{},[449,454],{"type":22,"tag":50,"props":450,"children":451},{},[452],{"type":28,"value":453},"Tooling matters as much as code",{"type":28,"value":455}," — Vite's superior DX made the whole migration feel faster",{"type":22,"tag":46,"props":457,"children":458},{},[459,464],{"type":22,"tag":50,"props":460,"children":461},{},[462],{"type":28,"value":463},"Go incremental",{"type":28,"value":465}," — don't try a big bang rewrite. Parallel migration kept the team shipping",{"type":22,"tag":46,"props":467,"children":468},{},[469,474],{"type":22,"tag":50,"props":470,"children":471},{},[472],{"type":28,"value":473},"Type safety is an enabler",{"type":28,"value":475}," — we spent 20% on migration, but gained 80% in error prevention",{"type":22,"tag":46,"props":477,"children":478},{},[479,484],{"type":22,"tag":50,"props":480,"children":481},{},[482],{"type":28,"value":483},"Test coverage reveals truth",{"type":28,"value":485}," — broken tests weren't a migration problem; they revealed logic bugs",{"type":22,"tag":46,"props":487,"children":488},{},[489,494],{"type":22,"tag":50,"props":490,"children":491},{},[492],{"type":28,"value":493},"Plan for edge cases",{"type":28,"value":495}," — our 10% \"final polish\" took 20% of the time (event handling, filters, etc.)",{"type":22,"tag":23,"props":497,"children":499},{"id":498},"what-wed-do-differently",[500],{"type":28,"value":501},"What We'd Do Differently",{"type":22,"tag":42,"props":503,"children":504},{},[505,515,525,535],{"type":22,"tag":46,"props":506,"children":507},{},[508,513],{"type":22,"tag":50,"props":509,"children":510},{},[511],{"type":28,"value":512},"Start with Vite first",{"type":28,"value":514}," — migrate tooling before components (the performance boost motivated the team)",{"type":22,"tag":46,"props":516,"children":517},{},[518,523],{"type":22,"tag":50,"props":519,"children":520},{},[521],{"type":28,"value":522},"Create better codemods",{"type":28,"value":524}," — we wrote several after the fact that would have saved days",{"type":22,"tag":46,"props":526,"children":527},{},[528,533],{"type":22,"tag":50,"props":529,"children":530},{},[531],{"type":28,"value":532},"Allocate 15-20% for unknowns",{"type":28,"value":534}," — we estimated 6 weeks; it took 8. That's normal for large migrations",{"type":22,"tag":46,"props":536,"children":537},{},[538,543],{"type":22,"tag":50,"props":539,"children":540},{},[541],{"type":28,"value":542},"Train junior engineers",{"type":28,"value":544}," — force 2-3 people to migrate components solo. They become experts quickly",{"type":22,"tag":31,"props":546,"children":547},{},[548],{"type":28,"value":549},"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":551,"depth":551,"links":552},2,[553,554,561,562,563],{"id":25,"depth":551,"text":29},{"id":102,"depth":551,"text":105,"children":555},[556,558,559,560],{"id":109,"depth":557,"text":112},3,{"id":169,"depth":557,"text":172},{"id":284,"depth":557,"text":287},{"id":365,"depth":557,"text":368},{"id":395,"depth":551,"text":398},{"id":439,"depth":551,"text":442},{"id":498,"depth":551,"text":501},"markdown","content:blog:vue-2-to-vue-3-migration-at-scale.md","content","blog\u002Fvue-2-to-vue-3-migration-at-scale.md","blog\u002Fvue-2-to-vue-3-migration-at-scale","md",[571,995,1575,2120,2925,3596,4027,4886,5349,6083],{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"updated":11,"readingTime":12,"tags":572,"featured":17,"body":573,"_type":564,"_id":565,"_source":566,"_file":567,"_stem":568,"_extension":569},[14,15,16],{"type":19,"children":574,"toc":983},[575,579,583,587,628,632,636,640,654,662,666,681,685,689,716,720,728,732,772,776,785,789,797,801,805,813,817,821,829,833,837,841,849,853,857,861,865,872,893,897,940,944,979],{"type":22,"tag":23,"props":576,"children":577},{"id":25},[578],{"type":28,"value":29},{"type":22,"tag":31,"props":580,"children":581},{},[582],{"type":28,"value":35},{"type":22,"tag":31,"props":584,"children":585},{},[586],{"type":28,"value":40},{"type":22,"tag":42,"props":588,"children":589},{},[590,598,606,614],{"type":22,"tag":46,"props":591,"children":592},{},[593,597],{"type":22,"tag":50,"props":594,"children":595},{},[596],{"type":28,"value":54},{"type":28,"value":56},{"type":22,"tag":46,"props":599,"children":600},{},[601,605],{"type":22,"tag":50,"props":602,"children":603},{},[604],{"type":28,"value":64},{"type":28,"value":66},{"type":22,"tag":46,"props":607,"children":608},{},[609,613],{"type":22,"tag":50,"props":610,"children":611},{},[612],{"type":28,"value":16},{"type":28,"value":75},{"type":22,"tag":46,"props":615,"children":616},{},[617,621,622,627],{"type":22,"tag":50,"props":618,"children":619},{},[620],{"type":28,"value":83},{"type":28,"value":85},{"type":22,"tag":87,"props":623,"children":625},{"className":624},[],[626],{"type":28,"value":92},{"type":28,"value":94},{"type":22,"tag":31,"props":629,"children":630},{},[631],{"type":28,"value":99},{"type":22,"tag":23,"props":633,"children":634},{"id":102},[635],{"type":28,"value":105},{"type":22,"tag":107,"props":637,"children":638},{"id":109},[639],{"type":28,"value":112},{"type":22,"tag":31,"props":641,"children":642},{},[643,644,648,649,653],{"type":28,"value":117},{"type":22,"tag":50,"props":645,"children":646},{},[647],{"type":28,"value":122},{"type":28,"value":124},{"type":22,"tag":50,"props":650,"children":651},{},[652],{"type":28,"value":129},{"type":28,"value":131},{"type":22,"tag":133,"props":655,"children":657},{"className":656,"code":137,"language":138,"meta":7},[136],[658],{"type":22,"tag":87,"props":659,"children":660},{"__ignoreMap":7},[661],{"type":28,"value":137},{"type":22,"tag":31,"props":663,"children":664},{},[665],{"type":28,"value":148},{"type":22,"tag":42,"props":667,"children":668},{},[669,673,677],{"type":22,"tag":46,"props":670,"children":671},{},[672],{"type":28,"value":156},{"type":22,"tag":46,"props":674,"children":675},{},[676],{"type":28,"value":161},{"type":22,"tag":46,"props":678,"children":679},{},[680],{"type":28,"value":166},{"type":22,"tag":107,"props":682,"children":683},{"id":169},[684],{"type":28,"value":172},{"type":22,"tag":31,"props":686,"children":687},{},[688],{"type":28,"value":177},{"type":22,"tag":179,"props":690,"children":691},{},[692,700,708],{"type":22,"tag":46,"props":693,"children":694},{},[695,699],{"type":22,"tag":50,"props":696,"children":697},{},[698],{"type":28,"value":189},{"type":28,"value":191},{"type":22,"tag":46,"props":701,"children":702},{},[703,707],{"type":22,"tag":50,"props":704,"children":705},{},[706],{"type":28,"value":199},{"type":28,"value":201},{"type":22,"tag":46,"props":709,"children":710},{},[711,715],{"type":22,"tag":50,"props":712,"children":713},{},[714],{"type":28,"value":209},{"type":28,"value":211},{"type":22,"tag":213,"props":717,"children":718},{"id":215},[719],{"type":28,"value":218},{"type":22,"tag":133,"props":721,"children":723},{"className":722,"code":222,"language":138,"meta":7},[136],[724],{"type":22,"tag":87,"props":725,"children":726},{"__ignoreMap":7},[727],{"type":28,"value":222},{"type":22,"tag":31,"props":729,"children":730},{},[731],{"type":28,"value":232},{"type":22,"tag":42,"props":733,"children":734},{},[735,743,751,759],{"type":22,"tag":46,"props":736,"children":737},{},[738,742],{"type":22,"tag":50,"props":739,"children":740},{},[741],{"type":28,"value":243},{"type":28,"value":245},{"type":22,"tag":46,"props":744,"children":745},{},[746,750],{"type":22,"tag":50,"props":747,"children":748},{},[749],{"type":28,"value":253},{"type":28,"value":255},{"type":22,"tag":46,"props":752,"children":753},{},[754,758],{"type":22,"tag":50,"props":755,"children":756},{},[757],{"type":28,"value":263},{"type":28,"value":265},{"type":22,"tag":46,"props":760,"children":761},{},[762,766,767],{"type":22,"tag":50,"props":763,"children":764},{},[765],{"type":28,"value":273},{"type":28,"value":275},{"type":22,"tag":87,"props":768,"children":770},{"className":769},[],[771],{"type":28,"value":281},{"type":22,"tag":107,"props":773,"children":774},{"id":284},[775],{"type":28,"value":287},{"type":22,"tag":31,"props":777,"children":778},{},[779,780,784],{"type":28,"value":292},{"type":22,"tag":294,"props":781,"children":782},{},[783],{"type":28,"value":298},{"type":28,"value":300},{"type":22,"tag":213,"props":786,"children":787},{"id":303},[788],{"type":28,"value":306},{"type":22,"tag":133,"props":790,"children":792},{"className":791,"code":311,"language":312,"meta":7},[310],[793],{"type":22,"tag":87,"props":794,"children":795},{"__ignoreMap":7},[796],{"type":28,"value":311},{"type":22,"tag":31,"props":798,"children":799},{},[800],{"type":28,"value":322},{"type":22,"tag":213,"props":802,"children":803},{"id":325},[804],{"type":28,"value":328},{"type":22,"tag":133,"props":806,"children":808},{"className":807,"code":332,"language":312,"meta":7},[310],[809],{"type":22,"tag":87,"props":810,"children":811},{"__ignoreMap":7},[812],{"type":28,"value":332},{"type":22,"tag":31,"props":814,"children":815},{},[816],{"type":28,"value":342},{"type":22,"tag":213,"props":818,"children":819},{"id":345},[820],{"type":28,"value":348},{"type":22,"tag":133,"props":822,"children":824},{"className":823,"code":352,"language":312,"meta":7},[310],[825],{"type":22,"tag":87,"props":826,"children":827},{"__ignoreMap":7},[828],{"type":28,"value":352},{"type":22,"tag":31,"props":830,"children":831},{},[832],{"type":28,"value":362},{"type":22,"tag":107,"props":834,"children":835},{"id":365},[836],{"type":28,"value":368},{"type":22,"tag":31,"props":838,"children":839},{},[840],{"type":28,"value":373},{"type":22,"tag":133,"props":842,"children":844},{"className":843,"code":377,"language":138,"meta":7},[136],[845],{"type":22,"tag":87,"props":846,"children":847},{"__ignoreMap":7},[848],{"type":28,"value":377},{"type":22,"tag":31,"props":850,"children":851},{},[852],{"type":28,"value":387},{"type":22,"tag":31,"props":854,"children":855},{},[856],{"type":28,"value":392},{"type":22,"tag":23,"props":858,"children":859},{"id":395},[860],{"type":28,"value":398},{"type":22,"tag":31,"props":862,"children":863},{},[864],{"type":28,"value":403},{"type":22,"tag":31,"props":866,"children":867},{},[868],{"type":22,"tag":50,"props":869,"children":870},{},[871],{"type":28,"value":411},{"type":22,"tag":42,"props":873,"children":874},{},[875,879,889],{"type":22,"tag":46,"props":876,"children":877},{},[878],{"type":28,"value":419},{"type":22,"tag":46,"props":880,"children":881},{},[882,883,888],{"type":28,"value":424},{"type":22,"tag":87,"props":884,"children":886},{"className":885},[],[887],{"type":28,"value":281},{"type":28,"value":431},{"type":22,"tag":46,"props":890,"children":891},{},[892],{"type":28,"value":436},{"type":22,"tag":23,"props":894,"children":895},{"id":439},[896],{"type":28,"value":442},{"type":22,"tag":179,"props":898,"children":899},{},[900,908,916,924,932],{"type":22,"tag":46,"props":901,"children":902},{},[903,907],{"type":22,"tag":50,"props":904,"children":905},{},[906],{"type":28,"value":453},{"type":28,"value":455},{"type":22,"tag":46,"props":909,"children":910},{},[911,915],{"type":22,"tag":50,"props":912,"children":913},{},[914],{"type":28,"value":463},{"type":28,"value":465},{"type":22,"tag":46,"props":917,"children":918},{},[919,923],{"type":22,"tag":50,"props":920,"children":921},{},[922],{"type":28,"value":473},{"type":28,"value":475},{"type":22,"tag":46,"props":925,"children":926},{},[927,931],{"type":22,"tag":50,"props":928,"children":929},{},[930],{"type":28,"value":483},{"type":28,"value":485},{"type":22,"tag":46,"props":933,"children":934},{},[935,939],{"type":22,"tag":50,"props":936,"children":937},{},[938],{"type":28,"value":493},{"type":28,"value":495},{"type":22,"tag":23,"props":941,"children":942},{"id":498},[943],{"type":28,"value":501},{"type":22,"tag":42,"props":945,"children":946},{},[947,955,963,971],{"type":22,"tag":46,"props":948,"children":949},{},[950,954],{"type":22,"tag":50,"props":951,"children":952},{},[953],{"type":28,"value":512},{"type":28,"value":514},{"type":22,"tag":46,"props":956,"children":957},{},[958,962],{"type":22,"tag":50,"props":959,"children":960},{},[961],{"type":28,"value":522},{"type":28,"value":524},{"type":22,"tag":46,"props":964,"children":965},{},[966,970],{"type":22,"tag":50,"props":967,"children":968},{},[969],{"type":28,"value":532},{"type":28,"value":534},{"type":22,"tag":46,"props":972,"children":973},{},[974,978],{"type":22,"tag":50,"props":975,"children":976},{},[977],{"type":28,"value":542},{"type":28,"value":544},{"type":22,"tag":31,"props":980,"children":981},{},[982],{"type":28,"value":549},{"title":7,"searchDepth":551,"depth":551,"links":984},[985,986,992,993,994],{"id":25,"depth":551,"text":29},{"id":102,"depth":551,"text":105,"children":987},[988,989,990,991],{"id":109,"depth":557,"text":112},{"id":169,"depth":557,"text":172},{"id":284,"depth":557,"text":287},{"id":365,"depth":557,"text":368},{"id":395,"depth":551,"text":398},{"id":439,"depth":551,"text":442},{"id":498,"depth":551,"text":501},{"_path":996,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":997,"description":998,"date":999,"updated":11,"readingTime":1000,"tags":1001,"featured":17,"body":1003,"_type":564,"_id":1572,"_source":566,"_file":1573,"_stem":1574,"_extension":569},"\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,[1002,16],"Nuxt",{"type":19,"children":1004,"toc":1557},[1005,1011,1023,1037,1043,1049,1054,1065,1070,1103,1109,1114,1123,1133,1151,1157,1162,1173,1178,1187,1200,1209,1219,1225,1230,1239,1244,1250,1255,1264,1275,1286,1291,1297,1302,1311,1316,1334,1339,1345,1350,1359,1364,1370,1375,1438,1450,1456,1461,1470,1475,1493,1499,1552],{"type":22,"tag":23,"props":1006,"children":1008},{"id":1007},"starting-point-a-slow-production-app",[1009],{"type":28,"value":1010},"Starting Point: A Slow Production App",{"type":22,"tag":31,"props":1012,"children":1013},{},[1014,1016,1021],{"type":28,"value":1015},"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":22,"tag":50,"props":1017,"children":1018},{},[1019],{"type":28,"value":1020},"4.2s",{"type":28,"value":1022}," on throttled mobile. By Google's standards, anything over 2.5s for LCP is failing, and every user felt it.",{"type":22,"tag":31,"props":1024,"children":1025},{},[1026,1028,1035],{"type":28,"value":1027},"The full program behind this post — what was tried, what was rejected, and how the results are enforced — is written up in the ",{"type":22,"tag":1029,"props":1030,"children":1032},"a",{"href":1031},"\u002Fcase-studies\u002Fsaas-performance-program",[1033],{"type":28,"value":1034},"SaaS performance case study",{"type":28,"value":1036},". This post is the generalized playbook.",{"type":22,"tag":23,"props":1038,"children":1040},{"id":1039},"the-optimization-playbook",[1041],{"type":28,"value":1042},"The Optimization Playbook",{"type":22,"tag":107,"props":1044,"children":1046},{"id":1045},"_1-analyze-with-lighthouse-bundle-analysis",[1047],{"type":28,"value":1048},"1. Analyze with Lighthouse & Bundle Analysis",{"type":22,"tag":31,"props":1050,"children":1051},{},[1052],{"type":28,"value":1053},"Before optimizing, we measured everything:",{"type":22,"tag":133,"props":1055,"children":1060},{"className":1056,"code":1058,"language":1059,"meta":7},[1057],"language-bash","npm run build --analyze\n# Generated visual bundle report\n\n# Lighthouse CI integration\nnpm install -D @lhci\u002Fcli@0.9.x\n","bash",[1061],{"type":22,"tag":87,"props":1062,"children":1063},{"__ignoreMap":7},[1064],{"type":28,"value":1058},{"type":22,"tag":31,"props":1066,"children":1067},{},[1068],{"type":28,"value":1069},"A typical analysis of an unoptimized app reveals something like this (illustrative example — run your own):",{"type":22,"tag":42,"props":1071,"children":1072},{},[1073,1083,1093],{"type":22,"tag":46,"props":1074,"children":1075},{},[1076,1081],{"type":22,"tag":50,"props":1077,"children":1078},{},[1079],{"type":28,"value":1080},"Main bundle",{"type":28,"value":1082},": a few hundred KB gzipped of code the first paint doesn't need",{"type":22,"tag":46,"props":1084,"children":1085},{},[1086,1091],{"type":22,"tag":50,"props":1087,"children":1088},{},[1089],{"type":28,"value":1090},"Vendor bundle",{"type":28,"value":1092},": duplicate versions of the same utility library, bundled separately",{"type":22,"tag":46,"props":1094,"children":1095},{},[1096,1101],{"type":22,"tag":50,"props":1097,"children":1098},{},[1099],{"type":28,"value":1100},"Images",{"type":28,"value":1102},": the largest single line item — no WebP, no lazy loading, full-resolution on mobile",{"type":22,"tag":107,"props":1104,"children":1106},{"id":1105},"_2-code-splitting-strategy",[1107],{"type":28,"value":1108},"2. Code Splitting Strategy",{"type":22,"tag":31,"props":1110,"children":1111},{},[1112],{"type":28,"value":1113},"Nuxt 3 has better defaults than Nuxt 2, but we needed aggressive splitting:",{"type":22,"tag":133,"props":1115,"children":1118},{"className":1116,"code":1117,"language":138,"meta":7},[136],"\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",[1119],{"type":22,"tag":87,"props":1120,"children":1121},{"__ignoreMap":7},[1122],{"type":28,"value":1117},{"type":22,"tag":31,"props":1124,"children":1125},{},[1126,1131],{"type":22,"tag":50,"props":1127,"children":1128},{},[1129],{"type":28,"value":1130},"What to expect",{"type":28,"value":1132}," (illustrative example — your numbers will differ):",{"type":22,"tag":42,"props":1134,"children":1135},{},[1136,1141,1146],{"type":22,"tag":46,"props":1137,"children":1138},{},[1139],{"type":28,"value":1140},"The main bundle drops to whatever the first route genuinely needs",{"type":22,"tag":46,"props":1142,"children":1143},{},[1144],{"type":28,"value":1145},"Each lazy route loads a small chunk on demand",{"type":22,"tag":46,"props":1147,"children":1148},{},[1149],{"type":28,"value":1150},"Routes render without waiting for full app initialization",{"type":22,"tag":107,"props":1152,"children":1154},{"id":1153},"_3-image-optimization",[1155],{"type":28,"value":1156},"3. Image Optimization",{"type":22,"tag":31,"props":1158,"children":1159},{},[1160],{"type":28,"value":1161},"This single change had the biggest impact:",{"type":22,"tag":133,"props":1163,"children":1168},{"className":1164,"code":1166,"language":1167,"meta":7},[1165],"language-vue","\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","vue",[1169],{"type":22,"tag":87,"props":1170,"children":1171},{"__ignoreMap":7},[1172],{"type":28,"value":1166},{"type":22,"tag":31,"props":1174,"children":1175},{},[1176],{"type":28,"value":1177},"Install the Nuxt Image module:",{"type":22,"tag":133,"props":1179,"children":1182},{"className":1180,"code":1181,"language":1059,"meta":7},[1057],"npm install @nuxt\u002Fimage\n",[1183],{"type":22,"tag":87,"props":1184,"children":1185},{"__ignoreMap":7},[1186],{"type":28,"value":1181},{"type":22,"tag":31,"props":1188,"children":1189},{},[1190,1192,1198],{"type":28,"value":1191},"Configure in ",{"type":22,"tag":87,"props":1193,"children":1195},{"className":1194},[],[1196],{"type":28,"value":1197},"nuxt.config.ts",{"type":28,"value":1199},":",{"type":22,"tag":133,"props":1201,"children":1204},{"className":1202,"code":1203,"language":138,"meta":7},[136],"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",[1205],{"type":22,"tag":87,"props":1206,"children":1207},{"__ignoreMap":7},[1208],{"type":28,"value":1203},{"type":22,"tag":31,"props":1210,"children":1211},{},[1212,1217],{"type":22,"tag":50,"props":1213,"children":1214},{},[1215],{"type":28,"value":1216},"Why this matters most",{"type":28,"value":1218},": 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":22,"tag":107,"props":1220,"children":1222},{"id":1221},"_4-server-side-rendering-ssr-optimization",[1223],{"type":28,"value":1224},"4. Server-Side Rendering (SSR) Optimization",{"type":22,"tag":31,"props":1226,"children":1227},{},[1228],{"type":28,"value":1229},"Nuxt 3's SSR is better, but configuration matters:",{"type":22,"tag":133,"props":1231,"children":1234},{"className":1232,"code":1233,"language":138,"meta":7},[136],"\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",[1235],{"type":22,"tag":87,"props":1236,"children":1237},{"__ignoreMap":7},[1238],{"type":28,"value":1233},{"type":22,"tag":31,"props":1240,"children":1241},{},[1242],{"type":28,"value":1243},"The result: time-to-first-byte drops sharply once cacheable responses stop hitting the origin (illustrative — TTFB depends heavily on your hosting).",{"type":22,"tag":107,"props":1245,"children":1247},{"id":1246},"_5-font-loading-optimization",[1248],{"type":28,"value":1249},"5. Font Loading Optimization",{"type":22,"tag":31,"props":1251,"children":1252},{},[1253],{"type":28,"value":1254},"Fonts are invisible but heavy. We optimized aggressively:",{"type":22,"tag":133,"props":1256,"children":1259},{"className":1257,"code":1258,"language":138,"meta":7},[136],"\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",[1260],{"type":22,"tag":87,"props":1261,"children":1262},{"__ignoreMap":7},[1263],{"type":28,"value":1258},{"type":22,"tag":31,"props":1265,"children":1266},{},[1267,1269,1274],{"type":28,"value":1268},"But the real win came from ",{"type":22,"tag":50,"props":1270,"children":1271},{},[1272],{"type":28,"value":1273},"font-display: swap",{"type":28,"value":1199},{"type":22,"tag":133,"props":1276,"children":1281},{"className":1277,"code":1279,"language":1280,"meta":7},[1278],"language-css","@import url('https:\u002F\u002Ffonts.googleapis.com\u002Fcss2?family=Inter:wght@400;500;600;700&display=swap');\n","css",[1282],{"type":22,"tag":87,"props":1283,"children":1284},{"__ignoreMap":7},[1285],{"type":28,"value":1279},{"type":22,"tag":31,"props":1287,"children":1288},{},[1289],{"type":28,"value":1290},"This tells the browser: \"Show text immediately with fallback, swap in custom font when ready.\" No invisible text delays.",{"type":22,"tag":107,"props":1292,"children":1294},{"id":1293},"_6-hydration-optimization",[1295],{"type":28,"value":1296},"6. Hydration Optimization",{"type":22,"tag":31,"props":1298,"children":1299},{},[1300],{"type":28,"value":1301},"This is a Nuxt 3 secret weapon. Partial hydration means not every component needs JavaScript:",{"type":22,"tag":133,"props":1303,"children":1306},{"className":1304,"code":1305,"language":1167,"meta":7},[1165],"\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",[1307],{"type":22,"tag":87,"props":1308,"children":1309},{"__ignoreMap":7},[1310],{"type":28,"value":1305},{"type":22,"tag":31,"props":1312,"children":1313},{},[1314],{"type":28,"value":1315},"Use it for:",{"type":22,"tag":42,"props":1317,"children":1318},{},[1319,1324,1329],{"type":22,"tag":46,"props":1320,"children":1321},{},[1322],{"type":28,"value":1323},"Hero sections (no JS needed)",{"type":22,"tag":46,"props":1325,"children":1326},{},[1327],{"type":28,"value":1328},"Blog content (pure markup)",{"type":22,"tag":46,"props":1330,"children":1331},{},[1332],{"type":28,"value":1333},"Static sidebars",{"type":22,"tag":31,"props":1335,"children":1336},{},[1337],{"type":28,"value":1338},"Static sections that skip hydration are JavaScript the browser never has to parse.",{"type":22,"tag":107,"props":1340,"children":1342},{"id":1341},"_7-caching-strategy",[1343],{"type":28,"value":1344},"7. Caching Strategy",{"type":22,"tag":31,"props":1346,"children":1347},{},[1348],{"type":28,"value":1349},"Implemented a tiered caching approach:",{"type":22,"tag":133,"props":1351,"children":1354},{"className":1352,"code":1353,"language":138,"meta":7},[136],"\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",[1355],{"type":22,"tag":87,"props":1356,"children":1357},{"__ignoreMap":7},[1358],{"type":28,"value":1353},{"type":22,"tag":31,"props":1360,"children":1361},{},[1362],{"type":28,"value":1363},"With a tiered cache, most repeat API reads never reach the database.",{"type":22,"tag":23,"props":1365,"children":1367},{"id":1366},"the-final-results",[1368],{"type":28,"value":1369},"The Final Results",{"type":22,"tag":31,"props":1371,"children":1372},{},[1373],{"type":28,"value":1374},"On the production program this playbook comes from, the headline result was:",{"type":22,"tag":1376,"props":1377,"children":1378},"table",{},[1379,1408],{"type":22,"tag":1380,"props":1381,"children":1382},"thead",{},[1383],{"type":22,"tag":1384,"props":1385,"children":1386},"tr",{},[1387,1393,1398,1403],{"type":22,"tag":1388,"props":1389,"children":1390},"th",{},[1391],{"type":28,"value":1392},"Metric",{"type":22,"tag":1388,"props":1394,"children":1395},{},[1396],{"type":28,"value":1397},"Before",{"type":22,"tag":1388,"props":1399,"children":1400},{},[1401],{"type":28,"value":1402},"After",{"type":22,"tag":1388,"props":1404,"children":1405},{},[1406],{"type":28,"value":1407},"Improvement",{"type":22,"tag":1409,"props":1410,"children":1411},"tbody",{},[1412],{"type":22,"tag":1384,"props":1413,"children":1414},{},[1415,1421,1425,1430],{"type":22,"tag":1416,"props":1417,"children":1418},"td",{},[1419],{"type":28,"value":1420},"Largest Contentful Paint",{"type":22,"tag":1416,"props":1422,"children":1423},{},[1424],{"type":28,"value":1020},{"type":22,"tag":1416,"props":1426,"children":1427},{},[1428],{"type":28,"value":1429},"2.5s",{"type":22,"tag":1416,"props":1431,"children":1432},{},[1433],{"type":22,"tag":50,"props":1434,"children":1435},{},[1436],{"type":28,"value":1437},"−40%",{"type":22,"tag":31,"props":1439,"children":1440},{},[1441,1443,1448],{"type":28,"value":1442},"The other vitals moved in the same direction; the LCP number is the one measured carefully enough to publish. The ",{"type":22,"tag":1029,"props":1444,"children":1445},{"href":1031},[1446],{"type":28,"value":1447},"case study",{"type":28,"value":1449}," covers how it was measured and enforced.",{"type":22,"tag":23,"props":1451,"children":1453},{"id":1452},"performance-budget-going-forward",[1454],{"type":28,"value":1455},"Performance Budget Going Forward",{"type":22,"tag":31,"props":1457,"children":1458},{},[1459],{"type":28,"value":1460},"We set strict budgets to prevent regressions:",{"type":22,"tag":133,"props":1462,"children":1465},{"className":1463,"code":1464,"language":1059,"meta":7},[1057],"# 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",[1466],{"type":22,"tag":87,"props":1467,"children":1468},{"__ignoreMap":7},[1469],{"type":28,"value":1464},{"type":22,"tag":31,"props":1471,"children":1472},{},[1473],{"type":28,"value":1474},"Thresholds:",{"type":22,"tag":42,"props":1476,"children":1477},{},[1478,1483,1488],{"type":22,"tag":46,"props":1479,"children":1480},{},[1481],{"type":28,"value":1482},"Main bundle: \u003C 200KB gzipped",{"type":22,"tag":46,"props":1484,"children":1485},{},[1486],{"type":28,"value":1487},"Per route: \u003C 80KB gzipped",{"type":22,"tag":46,"props":1489,"children":1490},{},[1491],{"type":28,"value":1492},"Images: \u003C 1MB total per page",{"type":22,"tag":23,"props":1494,"children":1496},{"id":1495},"key-takeaways",[1497],{"type":28,"value":1498},"Key Takeaways",{"type":22,"tag":179,"props":1500,"children":1501},{},[1502,1512,1522,1532,1542],{"type":22,"tag":46,"props":1503,"children":1504},{},[1505,1510],{"type":22,"tag":50,"props":1506,"children":1507},{},[1508],{"type":28,"value":1509},"Measure first",{"type":28,"value":1511}," — every optimization should be data-driven",{"type":22,"tag":46,"props":1513,"children":1514},{},[1515,1520],{"type":22,"tag":50,"props":1516,"children":1517},{},[1518],{"type":28,"value":1519},"Images are the lowest-hanging fruit",{"type":28,"value":1521}," — image handling routinely delivers the largest share of the wins",{"type":22,"tag":46,"props":1523,"children":1524},{},[1525,1530],{"type":22,"tag":50,"props":1526,"children":1527},{},[1528],{"type":28,"value":1529},"HTTP caching > code optimization",{"type":28,"value":1531}," — smart caching beats micro-optimizations",{"type":22,"tag":46,"props":1533,"children":1534},{},[1535,1540],{"type":22,"tag":50,"props":1536,"children":1537},{},[1538],{"type":28,"value":1539},"Nuxt 3's primitives are powerful",{"type":28,"value":1541}," — when used correctly, you don't need complex workarounds",{"type":22,"tag":46,"props":1543,"children":1544},{},[1545,1550],{"type":22,"tag":50,"props":1546,"children":1547},{},[1548],{"type":28,"value":1549},"Monitor in production",{"type":28,"value":1551}," — real-world metrics differ from labs. Use tools like Sentry, Vercel Analytics",{"type":22,"tag":31,"props":1553,"children":1554},{},[1555],{"type":28,"value":1556},"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":551,"depth":551,"links":1558},[1559,1560,1569,1570,1571],{"id":1007,"depth":551,"text":1010},{"id":1039,"depth":551,"text":1042,"children":1561},[1562,1563,1564,1565,1566,1567,1568],{"id":1045,"depth":557,"text":1048},{"id":1105,"depth":557,"text":1108},{"id":1153,"depth":557,"text":1156},{"id":1221,"depth":557,"text":1224},{"id":1246,"depth":557,"text":1249},{"id":1293,"depth":557,"text":1296},{"id":1341,"depth":557,"text":1344},{"id":1366,"depth":551,"text":1369},{"id":1452,"depth":551,"text":1455},{"id":1495,"depth":551,"text":1498},"content:blog:nuxt-3-performance-optimization-guide.md","blog\u002Fnuxt-3-performance-optimization-guide.md","blog\u002Fnuxt-3-performance-optimization-guide",{"_path":1576,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":1577,"description":1578,"date":1579,"updated":11,"readingTime":1580,"tags":1581,"featured":6,"body":1582,"_type":564,"_id":2117,"_source":566,"_file":2118,"_stem":2119,"_extension":569},"\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,[14,15],{"type":19,"children":1583,"toc":2103},[1584,1590,1595,1600,1623,1636,1642,1648,1653,1661,1666,1689,1695,1700,1709,1714,1757,1763,1768,1777,1782,1805,1811,1816,1822,1831,1837,1846,1852,1857,1866,1872,1877,1886,1891,1924,1929,1940,1946,1958,2011,2017,2029,2035,2098],{"type":22,"tag":23,"props":1585,"children":1587},{"id":1586},"the-problem-with-ad-hoc-components",[1588],{"type":28,"value":1589},"The Problem with Ad-Hoc Components",{"type":22,"tag":31,"props":1591,"children":1592},{},[1593],{"type":28,"value":1594},"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":22,"tag":31,"props":1596,"children":1597},{},[1598],{"type":28,"value":1599},"Inconsistency was costing us:",{"type":22,"tag":42,"props":1601,"children":1602},{},[1603,1608,1613,1618],{"type":22,"tag":46,"props":1604,"children":1605},{},[1606],{"type":28,"value":1607},"New team members took weeks to learn existing patterns",{"type":22,"tag":46,"props":1609,"children":1610},{},[1611],{"type":28,"value":1612},"Components were re-implemented multiple times across products",{"type":22,"tag":46,"props":1614,"children":1615},{},[1616],{"type":28,"value":1617},"No single source of truth for design decisions",{"type":22,"tag":46,"props":1619,"children":1620},{},[1621],{"type":28,"value":1622},"Design-to-dev handoff took meetings just to clarify prop names",{"type":22,"tag":31,"props":1624,"children":1625},{},[1626,1628,1634],{"type":28,"value":1627},"We needed a design system. The production version of this story — with the monorepo layout, versioning policy, and adoption mechanics — is in the ",{"type":22,"tag":1029,"props":1629,"children":1631},{"href":1630},"\u002Fcase-studies\u002Fmonorepo-design-system",[1632],{"type":28,"value":1633},"monorepo design system case study",{"type":28,"value":1635},".",{"type":22,"tag":23,"props":1637,"children":1639},{"id":1638},"architecture-the-foundation",[1640],{"type":28,"value":1641},"Architecture: The Foundation",{"type":22,"tag":107,"props":1643,"children":1645},{"id":1644},"_1-monorepo-structure",[1646],{"type":28,"value":1647},"1. Monorepo Structure",{"type":22,"tag":31,"props":1649,"children":1650},{},[1651],{"type":28,"value":1652},"We used pnpm workspaces:",{"type":22,"tag":133,"props":1654,"children":1656},{"code":1655},"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",[1657],{"type":22,"tag":87,"props":1658,"children":1659},{"__ignoreMap":7},[1660],{"type":28,"value":1655},{"type":22,"tag":31,"props":1662,"children":1663},{},[1664],{"type":28,"value":1665},"Benefits:",{"type":22,"tag":42,"props":1667,"children":1668},{},[1669,1674,1679,1684],{"type":22,"tag":46,"props":1670,"children":1671},{},[1672],{"type":28,"value":1673},"Monorepo enforces consistency",{"type":22,"tag":46,"props":1675,"children":1676},{},[1677],{"type":28,"value":1678},"Shared tooling configuration",{"type":22,"tag":46,"props":1680,"children":1681},{},[1682],{"type":28,"value":1683},"Easy to test components in isolation",{"type":22,"tag":46,"props":1685,"children":1686},{},[1687],{"type":28,"value":1688},"Clear dependency management",{"type":22,"tag":107,"props":1690,"children":1692},{"id":1691},"_2-component-api-design",[1693],{"type":28,"value":1694},"2. Component API Design",{"type":22,"tag":31,"props":1696,"children":1697},{},[1698],{"type":28,"value":1699},"We established strict rules for component props:",{"type":22,"tag":133,"props":1701,"children":1704},{"code":1702,"language":138,"meta":7,"className":1703},"\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",[136],[1705],{"type":22,"tag":87,"props":1706,"children":1707},{"__ignoreMap":7},[1708],{"type":28,"value":1702},{"type":22,"tag":31,"props":1710,"children":1711},{},[1712],{"type":28,"value":1713},"Guidelines:",{"type":22,"tag":179,"props":1715,"children":1716},{},[1717,1727,1737,1747],{"type":22,"tag":46,"props":1718,"children":1719},{},[1720,1725],{"type":22,"tag":50,"props":1721,"children":1722},{},[1723],{"type":28,"value":1724},"Keep props count \u003C 8",{"type":28,"value":1726}," — if you need more, you're probably missing a abstraction",{"type":22,"tag":46,"props":1728,"children":1729},{},[1730,1735],{"type":22,"tag":50,"props":1731,"children":1732},{},[1733],{"type":28,"value":1734},"Use CSS variables for styling",{"type":28,"value":1736}," — don't expose every style property",{"type":22,"tag":46,"props":1738,"children":1739},{},[1740,1745],{"type":22,"tag":50,"props":1741,"children":1742},{},[1743],{"type":28,"value":1744},"Provide sensible defaults",{"type":28,"value":1746}," — most components should work with zero props",{"type":22,"tag":46,"props":1748,"children":1749},{},[1750,1755],{"type":22,"tag":50,"props":1751,"children":1752},{},[1753],{"type":28,"value":1754},"Document the \"why\"",{"type":28,"value":1756}," — every prop should have a reason",{"type":22,"tag":107,"props":1758,"children":1760},{"id":1759},"_3-design-tokens",[1761],{"type":28,"value":1762},"3. Design Tokens",{"type":22,"tag":31,"props":1764,"children":1765},{},[1766],{"type":28,"value":1767},"Tokens are the single source of truth:",{"type":22,"tag":133,"props":1769,"children":1772},{"code":1770,"language":138,"meta":7,"className":1771},"\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",[136],[1773],{"type":22,"tag":87,"props":1774,"children":1775},{"__ignoreMap":7},[1776],{"type":28,"value":1770},{"type":22,"tag":31,"props":1778,"children":1779},{},[1780],{"type":28,"value":1781},"This single file becomes the source of truth for:",{"type":22,"tag":42,"props":1783,"children":1784},{},[1785,1790,1795,1800],{"type":22,"tag":46,"props":1786,"children":1787},{},[1788],{"type":28,"value":1789},"Figma design tokens",{"type":22,"tag":46,"props":1791,"children":1792},{},[1793],{"type":28,"value":1794},"CSS variables",{"type":22,"tag":46,"props":1796,"children":1797},{},[1798],{"type":28,"value":1799},"Type definitions",{"type":22,"tag":46,"props":1801,"children":1802},{},[1803],{"type":28,"value":1804},"Storybook theming",{"type":22,"tag":107,"props":1806,"children":1808},{"id":1807},"_4-component-patterns",[1809],{"type":28,"value":1810},"4. Component Patterns",{"type":22,"tag":31,"props":1812,"children":1813},{},[1814],{"type":28,"value":1815},"We established patterns for common scenarios:",{"type":22,"tag":213,"props":1817,"children":1819},{"id":1818},"simple-component",[1820],{"type":28,"value":1821},"Simple Component",{"type":22,"tag":133,"props":1823,"children":1826},{"code":1824,"language":1167,"meta":7,"className":1825},"\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",[1165],[1827],{"type":22,"tag":87,"props":1828,"children":1829},{"__ignoreMap":7},[1830],{"type":28,"value":1824},{"type":22,"tag":213,"props":1832,"children":1834},{"id":1833},"compound-component",[1835],{"type":28,"value":1836},"Compound Component",{"type":22,"tag":133,"props":1838,"children":1841},{"code":1839,"language":1167,"meta":7,"className":1840},"\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",[1165],[1842],{"type":22,"tag":87,"props":1843,"children":1844},{"__ignoreMap":7},[1845],{"type":28,"value":1839},{"type":22,"tag":23,"props":1847,"children":1849},{"id":1848},"documentation-storybook",[1850],{"type":28,"value":1851},"Documentation & Storybook",{"type":22,"tag":31,"props":1853,"children":1854},{},[1855],{"type":28,"value":1856},"Storybook is critical for design system success:",{"type":22,"tag":133,"props":1858,"children":1861},{"code":1859,"language":138,"meta":7,"className":1860},"\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",[136],[1862],{"type":22,"tag":87,"props":1863,"children":1864},{"__ignoreMap":7},[1865],{"type":28,"value":1859},{"type":22,"tag":23,"props":1867,"children":1869},{"id":1868},"versioning-releases",[1870],{"type":28,"value":1871},"Versioning & Releases",{"type":22,"tag":31,"props":1873,"children":1874},{},[1875],{"type":28,"value":1876},"Design systems are libraries. Treat them like it:",{"type":22,"tag":133,"props":1878,"children":1881},{"code":1879,"language":1059,"meta":7,"className":1880},"# 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",[1057],[1882],{"type":22,"tag":87,"props":1883,"children":1884},{"__ignoreMap":7},[1885],{"type":28,"value":1879},{"type":22,"tag":31,"props":1887,"children":1888},{},[1889],{"type":28,"value":1890},"We published:",{"type":22,"tag":42,"props":1892,"children":1893},{},[1894,1904,1914],{"type":22,"tag":46,"props":1895,"children":1896},{},[1897,1902],{"type":22,"tag":50,"props":1898,"children":1899},{},[1900],{"type":28,"value":1901},"v1.0.0",{"type":28,"value":1903}," — the core component set\n",{"type":22,"tag":46,"props":1905,"children":1906},{},[1907,1912],{"type":22,"tag":50,"props":1908,"children":1909},{},[1910],{"type":28,"value":1911},"v1.2.3",{"type":28,"value":1913}," — new tokens, backward compatible",{"type":22,"tag":46,"props":1915,"children":1916},{},[1917,1922],{"type":22,"tag":50,"props":1918,"children":1919},{},[1920],{"type":28,"value":1921},"v2.0.0",{"type":28,"value":1923}," — redesign with breaking changes",{"type":22,"tag":31,"props":1925,"children":1926},{},[1927],{"type":28,"value":1928},"Each version went to npm registry. Teams pinned versions:",{"type":22,"tag":133,"props":1930,"children":1935},{"code":1931,"language":1932,"meta":7,"className":1933},"{\n  \"dependencies\": {\n    \"@acme\u002Fdesign-system\": \"^1.2.0\"\n  }\n}\n","json",[1934],"language-json",[1936],{"type":22,"tag":87,"props":1937,"children":1938},{"__ignoreMap":7},[1939],{"type":28,"value":1931},{"type":22,"tag":23,"props":1941,"children":1943},{"id":1942},"adoption-strategy",[1944],{"type":28,"value":1945},"Adoption Strategy",{"type":22,"tag":31,"props":1947,"children":1948},{},[1949,1951,1956],{"type":28,"value":1950},"Getting teams to ",{"type":22,"tag":294,"props":1952,"children":1953},{},[1954],{"type":28,"value":1955},"use",{"type":28,"value":1957}," the system was as important as building it:",{"type":22,"tag":179,"props":1959,"children":1960},{},[1961,1971,1981,1991,2001],{"type":22,"tag":46,"props":1962,"children":1963},{},[1964,1969],{"type":22,"tag":50,"props":1965,"children":1966},{},[1967],{"type":28,"value":1968},"Dogfood internally",{"type":28,"value":1970}," — our flagship product used v1.0 before public release",{"type":22,"tag":46,"props":1972,"children":1973},{},[1974,1979],{"type":22,"tag":50,"props":1975,"children":1976},{},[1977],{"type":28,"value":1978},"Create migration guides",{"type":28,"value":1980}," — \"How to replace your Button with DesignSystemButton\"",{"type":22,"tag":46,"props":1982,"children":1983},{},[1984,1989],{"type":22,"tag":50,"props":1985,"children":1986},{},[1987],{"type":28,"value":1988},"Celebrate wins",{"type":28,"value":1990}," — \"we deleted three duplicate Button implementations this sprint\" beats any adoption mandate",{"type":22,"tag":46,"props":1992,"children":1993},{},[1994,1999],{"type":22,"tag":50,"props":1995,"children":1996},{},[1997],{"type":28,"value":1998},"Make it easier to use than not",{"type":28,"value":2000}," — publish to npm, show install in docs",{"type":22,"tag":46,"props":2002,"children":2003},{},[2004,2009],{"type":22,"tag":50,"props":2005,"children":2006},{},[2007],{"type":28,"value":2008},"Maintain it publicly",{"type":28,"value":2010}," — commit to performance, accessibility, TypeScript support",{"type":22,"tag":23,"props":2012,"children":2014},{"id":2013},"adoption-metrics",[2015],{"type":28,"value":2016},"Adoption Metrics",{"type":22,"tag":31,"props":2018,"children":2019},{},[2020,2022,2027],{"type":28,"value":2021},"The measured outcome on the production system this post draws from: ",{"type":22,"tag":50,"props":2023,"children":2024},{},[2025],{"type":28,"value":2026},"20% faster feature delivery",{"type":28,"value":2028}," on projects consuming the library — teams assemble features from documented components instead of rebuilding primitives.",{"type":22,"tag":23,"props":2030,"children":2032},{"id":2031},"key-lessons",[2033],{"type":28,"value":2034},"Key Lessons",{"type":22,"tag":179,"props":2036,"children":2037},{},[2038,2048,2058,2068,2078,2088],{"type":22,"tag":46,"props":2039,"children":2040},{},[2041,2046],{"type":22,"tag":50,"props":2042,"children":2043},{},[2044],{"type":28,"value":2045},"Tokens are the foundation",{"type":28,"value":2047}," — invest heavily in design tokens before components",{"type":22,"tag":46,"props":2049,"children":2050},{},[2051,2056],{"type":22,"tag":50,"props":2052,"children":2053},{},[2054],{"type":28,"value":2055},"API design is everything",{"type":28,"value":2057}," — a good API is adopted naturally; a bad one requires enforcement",{"type":22,"tag":46,"props":2059,"children":2060},{},[2061,2066],{"type":22,"tag":50,"props":2062,"children":2063},{},[2064],{"type":28,"value":2065},"Storybook isn't optional",{"type":28,"value":2067}," — it's how designers and developers communicate",{"type":22,"tag":46,"props":2069,"children":2070},{},[2071,2076],{"type":22,"tag":50,"props":2072,"children":2073},{},[2074],{"type":28,"value":2075},"Version properly",{"type":28,"value":2077}," — teams need confidence that upgrades won't break things",{"type":22,"tag":46,"props":2079,"children":2080},{},[2081,2086],{"type":22,"tag":50,"props":2082,"children":2083},{},[2084],{"type":28,"value":2085},"One person owns it",{"type":28,"value":2087}," — design systems need a maintainer, not a committee",{"type":22,"tag":46,"props":2089,"children":2090},{},[2091,2096],{"type":22,"tag":50,"props":2092,"children":2093},{},[2094],{"type":28,"value":2095},"Measure success by adoption",{"type":28,"value":2097}," — a perfect system nobody uses is useless",{"type":22,"tag":31,"props":2099,"children":2100},{},[2101],{"type":28,"value":2102},"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":551,"depth":551,"links":2104},[2105,2106,2112,2113,2114,2115,2116],{"id":1586,"depth":551,"text":1589},{"id":1638,"depth":551,"text":1641,"children":2107},[2108,2109,2110,2111],{"id":1644,"depth":557,"text":1647},{"id":1691,"depth":557,"text":1694},{"id":1759,"depth":557,"text":1762},{"id":1807,"depth":557,"text":1810},{"id":1848,"depth":551,"text":1851},{"id":1868,"depth":551,"text":1871},{"id":1942,"depth":551,"text":1945},{"id":2013,"depth":551,"text":2016},{"id":2031,"depth":551,"text":2034},"content:blog:building-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue.md","blog\u002Fbuilding-scalable-design-systems-vue",{"_path":2121,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":2122,"description":2123,"date":2124,"updated":11,"readingTime":2125,"tags":2126,"featured":6,"body":2127,"_type":564,"_id":2922,"_source":566,"_file":2923,"_stem":2924,"_extension":569},"\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,[15,16],{"type":19,"children":2128,"toc":2900},[2129,2135,2140,2193,2198,2204,2209,2217,2223,2276,2282,2325,2330,2336,2341,2349,2354,2377,2383,2389,2398,2404,2413,2418,2441,2447,2452,2505,2511,2516,2525,2529,2572,2578,2583,2592,2598,2604,2612,2617,2626,2632,2641,2647,2652,2660,2666,2671,2679,2684,2690,2695,2746,2752,2757,2781,2786,2792,2797,2895],{"type":22,"tag":23,"props":2130,"children":2132},{"id":2131},"the-scaling-problem",[2133],{"type":28,"value":2134},"The Scaling Problem",{"type":22,"tag":31,"props":2136,"children":2137},{},[2138],{"type":28,"value":2139},"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":22,"tag":42,"props":2141,"children":2142},{},[2143,2153,2163,2173,2183],{"type":22,"tag":46,"props":2144,"children":2145},{},[2146,2151],{"type":22,"tag":50,"props":2147,"children":2148},{},[2149],{"type":28,"value":2150},"Git conflicts",{"type":28,"value":2152}," — 20+ per day from different teams modifying the same files",{"type":22,"tag":46,"props":2154,"children":2155},{},[2156,2161],{"type":22,"tag":50,"props":2157,"children":2158},{},[2159],{"type":28,"value":2160},"Deployment bottlenecks",{"type":28,"value":2162}," — one bug in team A's feature blocked team B's release",{"type":22,"tag":46,"props":2164,"children":2165},{},[2166,2171],{"type":22,"tag":50,"props":2167,"children":2168},{},[2169],{"type":28,"value":2170},"Code ownership unclear",{"type":28,"value":2172}," — who owns the form validation layer? Three people claim they do",{"type":22,"tag":46,"props":2174,"children":2175},{},[2176,2181],{"type":22,"tag":50,"props":2177,"children":2178},{},[2179],{"type":28,"value":2180},"Bundle bloat",{"type":28,"value":2182}," — features for team A's customers were loading for team B's customers",{"type":22,"tag":46,"props":2184,"children":2185},{},[2186,2191],{"type":22,"tag":50,"props":2187,"children":2188},{},[2189],{"type":28,"value":2190},"Development friction",{"type":28,"value":2192}," — new team members took 4 weeks to understand the codebase",{"type":22,"tag":31,"props":2194,"children":2195},{},[2196],{"type":28,"value":2197},"Something had to change.",{"type":22,"tag":23,"props":2199,"children":2201},{"id":2200},"option-1-monorepo-what-we-chose",[2202],{"type":28,"value":2203},"Option 1: Monorepo (What We Chose)",{"type":22,"tag":31,"props":2205,"children":2206},{},[2207],{"type":28,"value":2208},"We chose a monorepo structure with pnpm workspaces:",{"type":22,"tag":133,"props":2210,"children":2212},{"code":2211},"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",[2213],{"type":22,"tag":87,"props":2214,"children":2215},{"__ignoreMap":7},[2216],{"type":28,"value":2211},{"type":22,"tag":107,"props":2218,"children":2220},{"id":2219},"monorepo-benefits",[2221],{"type":28,"value":2222},"Monorepo Benefits",{"type":22,"tag":42,"props":2224,"children":2225},{},[2226,2236,2246,2256,2266],{"type":22,"tag":46,"props":2227,"children":2228},{},[2229,2234],{"type":22,"tag":50,"props":2230,"children":2231},{},[2232],{"type":28,"value":2233},"Single source of truth",{"type":28,"value":2235}," for shared code",{"type":22,"tag":46,"props":2237,"children":2238},{},[2239,2244],{"type":22,"tag":50,"props":2240,"children":2241},{},[2242],{"type":28,"value":2243},"Atomic commits",{"type":28,"value":2245}," — can update multiple packages together",{"type":22,"tag":46,"props":2247,"children":2248},{},[2249,2254],{"type":22,"tag":50,"props":2250,"children":2251},{},[2252],{"type":28,"value":2253},"Shared CI\u002FCD",{"type":28,"value":2255}," — one build pipeline for all apps",{"type":22,"tag":46,"props":2257,"children":2258},{},[2259,2264],{"type":22,"tag":50,"props":2260,"children":2261},{},[2262],{"type":28,"value":2263},"Consistent tooling",{"type":28,"value":2265}," — eslint, prettier, typescript config",{"type":22,"tag":46,"props":2267,"children":2268},{},[2269,2274],{"type":22,"tag":50,"props":2270,"children":2271},{},[2272],{"type":28,"value":2273},"Easy refactoring",{"type":28,"value":2275}," — move code between apps without npm publish",{"type":22,"tag":107,"props":2277,"children":2279},{"id":2278},"monorepo-drawbacks",[2280],{"type":28,"value":2281},"Monorepo Drawbacks",{"type":22,"tag":42,"props":2283,"children":2284},{},[2285,2295,2305,2315],{"type":22,"tag":46,"props":2286,"children":2287},{},[2288,2293],{"type":22,"tag":50,"props":2289,"children":2290},{},[2291],{"type":28,"value":2292},"Build complexity",{"type":28,"value":2294}," — must rebuild only changed packages",{"type":22,"tag":46,"props":2296,"children":2297},{},[2298,2303],{"type":22,"tag":50,"props":2299,"children":2300},{},[2301],{"type":28,"value":2302},"Dependency management",{"type":28,"value":2304}," — circular dependencies possible",{"type":22,"tag":46,"props":2306,"children":2307},{},[2308,2313],{"type":22,"tag":50,"props":2309,"children":2310},{},[2311],{"type":28,"value":2312},"Harder to open-source",{"type":28,"value":2314}," — internal code in same repo",{"type":22,"tag":46,"props":2316,"children":2317},{},[2318,2323],{"type":22,"tag":50,"props":2319,"children":2320},{},[2321],{"type":28,"value":2322},"Steeper learning curve",{"type":28,"value":2324}," — new engineers need to understand workspace structure",{"type":22,"tag":31,"props":2326,"children":2327},{},[2328],{"type":28,"value":2329},"For us, benefits outweighed drawbacks.",{"type":22,"tag":23,"props":2331,"children":2333},{"id":2332},"option-2-micro-frontends-the-alternative",[2334],{"type":28,"value":2335},"Option 2: Micro-Frontends (The Alternative)",{"type":22,"tag":31,"props":2337,"children":2338},{},[2339],{"type":28,"value":2340},"For teams that want complete independence, micro-frontends are powerful:",{"type":22,"tag":133,"props":2342,"children":2344},{"code":2343},"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",[2345],{"type":22,"tag":87,"props":2346,"children":2347},{"__ignoreMap":7},[2348],{"type":28,"value":2343},{"type":22,"tag":31,"props":2350,"children":2351},{},[2352],{"type":28,"value":2353},"Each micro-frontend:",{"type":22,"tag":42,"props":2355,"children":2356},{},[2357,2362,2367,2372],{"type":22,"tag":46,"props":2358,"children":2359},{},[2360],{"type":28,"value":2361},"Has its own git repo",{"type":22,"tag":46,"props":2363,"children":2364},{},[2365],{"type":28,"value":2366},"Deploys independently",{"type":22,"tag":46,"props":2368,"children":2369},{},[2370],{"type":28,"value":2371},"Ships its own bundle",{"type":22,"tag":46,"props":2373,"children":2374},{},[2375],{"type":28,"value":2376},"Uses shared design tokens + api client",{"type":22,"tag":107,"props":2378,"children":2380},{"id":2379},"micro-frontend-patterns",[2381],{"type":28,"value":2382},"Micro-Frontend Patterns",{"type":22,"tag":213,"props":2384,"children":2386},{"id":2385},"_1-module-federation-webpack-5-vite",[2387],{"type":28,"value":2388},"1. Module Federation (Webpack 5 \u002F Vite)",{"type":22,"tag":133,"props":2390,"children":2393},{"code":2391,"language":138,"meta":7,"className":2392},"\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",[136],[2394],{"type":22,"tag":87,"props":2395,"children":2396},{"__ignoreMap":7},[2397],{"type":28,"value":2391},{"type":22,"tag":213,"props":2399,"children":2401},{"id":2400},"_2-url-based-routing",[2402],{"type":28,"value":2403},"2. URL-based Routing",{"type":22,"tag":133,"props":2405,"children":2408},{"code":2406,"language":1167,"meta":7,"className":2407},"\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",[1165],[2409],{"type":22,"tag":87,"props":2410,"children":2411},{"__ignoreMap":7},[2412],{"type":28,"value":2406},{"type":22,"tag":31,"props":2414,"children":2415},{},[2416],{"type":28,"value":2417},"Each micro-frontend is a complete app that can:",{"type":22,"tag":42,"props":2419,"children":2420},{},[2421,2426,2431,2436],{"type":22,"tag":46,"props":2422,"children":2423},{},[2424],{"type":28,"value":2425},"Deploy independently",{"type":22,"tag":46,"props":2427,"children":2428},{},[2429],{"type":28,"value":2430},"Use different versions of dependencies (if needed)",{"type":22,"tag":46,"props":2432,"children":2433},{},[2434],{"type":28,"value":2435},"Be owned by a separate team",{"type":22,"tag":46,"props":2437,"children":2438},{},[2439],{"type":28,"value":2440},"Scale independently",{"type":22,"tag":23,"props":2442,"children":2444},{"id":2443},"our-monorepo-decision",[2445],{"type":28,"value":2446},"Our Monorepo Decision",{"type":22,"tag":31,"props":2448,"children":2449},{},[2450],{"type":28,"value":2451},"We chose monorepo because:",{"type":22,"tag":179,"props":2453,"children":2454},{},[2455,2465,2475,2485,2495],{"type":22,"tag":46,"props":2456,"children":2457},{},[2458,2463],{"type":22,"tag":50,"props":2459,"children":2460},{},[2461],{"type":28,"value":2462},"Team maturity",{"type":28,"value":2464}," — all teams understood git and npm workspaces",{"type":22,"tag":46,"props":2466,"children":2467},{},[2468,2473],{"type":22,"tag":50,"props":2469,"children":2470},{},[2471],{"type":28,"value":2472},"Shared design system",{"type":28,"value":2474}," — critical for consistent UX",{"type":22,"tag":46,"props":2476,"children":2477},{},[2478,2483],{"type":22,"tag":50,"props":2479,"children":2480},{},[2481],{"type":28,"value":2482},"Single deployment",{"type":28,"value":2484}," — easier for our ops team",{"type":22,"tag":46,"props":2486,"children":2487},{},[2488,2493],{"type":22,"tag":50,"props":2489,"children":2490},{},[2491],{"type":28,"value":2492},"Developer experience",{"type":28,"value":2494}," — IDE support better in monorepo",{"type":22,"tag":46,"props":2496,"children":2497},{},[2498,2503],{"type":22,"tag":50,"props":2499,"children":2500},{},[2501],{"type":28,"value":2502},"Build optimization",{"type":28,"value":2504}," — turborepo handles incremental builds well",{"type":22,"tag":23,"props":2506,"children":2508},{"id":2507},"implementation-turborepo",[2509],{"type":28,"value":2510},"Implementation: Turborepo",{"type":22,"tag":31,"props":2512,"children":2513},{},[2514],{"type":28,"value":2515},"We use Turborepo for build orchestration:",{"type":22,"tag":133,"props":2517,"children":2520},{"code":2518,"language":1932,"meta":7,"className":2519},"{\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",[1934],[2521],{"type":22,"tag":87,"props":2522,"children":2523},{"__ignoreMap":7},[2524],{"type":28,"value":2518},{"type":22,"tag":31,"props":2526,"children":2527},{},[2528],{"type":28,"value":1665},{"type":22,"tag":42,"props":2530,"children":2531},{},[2532,2542,2552,2562],{"type":22,"tag":46,"props":2533,"children":2534},{},[2535,2540],{"type":22,"tag":50,"props":2536,"children":2537},{},[2538],{"type":28,"value":2539},"Parallel execution",{"type":28,"value":2541}," — builds run in parallel when possible",{"type":22,"tag":46,"props":2543,"children":2544},{},[2545,2550],{"type":22,"tag":50,"props":2546,"children":2547},{},[2548],{"type":28,"value":2549},"Task pipelines",{"type":28,"value":2551}," — test only runs after build",{"type":22,"tag":46,"props":2553,"children":2554},{},[2555,2560],{"type":22,"tag":50,"props":2556,"children":2557},{},[2558],{"type":28,"value":2559},"Caching",{"type":28,"value":2561}," — skips tasks that haven't changed",{"type":22,"tag":46,"props":2563,"children":2564},{},[2565,2570],{"type":22,"tag":50,"props":2566,"children":2567},{},[2568],{"type":28,"value":2569},"CI\u002FCD integration",{"type":28,"value":2571}," — auto-detects changed packages",{"type":22,"tag":23,"props":2573,"children":2575},{"id":2574},"state-management-at-scale",[2576],{"type":28,"value":2577},"State Management at Scale",{"type":22,"tag":31,"props":2579,"children":2580},{},[2581],{"type":28,"value":2582},"Pinia (Vue's official state management) handles monorepo scales well:",{"type":22,"tag":133,"props":2584,"children":2587},{"code":2585,"language":138,"meta":7,"className":2586},"\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",[136],[2588],{"type":22,"tag":87,"props":2589,"children":2590},{"__ignoreMap":7},[2591],{"type":28,"value":2585},{"type":22,"tag":23,"props":2593,"children":2595},{"id":2594},"preventing-common-pitfalls",[2596],{"type":28,"value":2597},"Preventing Common Pitfalls",{"type":22,"tag":107,"props":2599,"children":2601},{"id":2600},"_1-circular-dependencies",[2602],{"type":28,"value":2603},"1. Circular Dependencies",{"type":22,"tag":133,"props":2605,"children":2607},{"code":2606},"Bad: dashboard → api-client → shared-utils → dashboard\n",[2608],{"type":22,"tag":87,"props":2609,"children":2610},{"__ignoreMap":7},[2611],{"type":28,"value":2606},{"type":22,"tag":31,"props":2613,"children":2614},{},[2615],{"type":28,"value":2616},"Solution: Enforce dependency graph with eslint:",{"type":22,"tag":133,"props":2618,"children":2621},{"code":2619,"language":312,"meta":7,"className":2620},"\u002F\u002F .eslintrc.js\n{\n  \"rules\": {\n    \"import\u002Fno-cycle\": \"error\"\n  }\n}\n",[310],[2622],{"type":22,"tag":87,"props":2623,"children":2624},{"__ignoreMap":7},[2625],{"type":28,"value":2619},{"type":22,"tag":107,"props":2627,"children":2629},{"id":2628},"_2-shared-dependency-versions",[2630],{"type":28,"value":2631},"2. Shared Dependency Versions",{"type":22,"tag":133,"props":2633,"children":2636},{"code":2634,"language":138,"meta":7,"className":2635},"\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",[136],[2637],{"type":22,"tag":87,"props":2638,"children":2639},{"__ignoreMap":7},[2640],{"type":28,"value":2634},{"type":22,"tag":107,"props":2642,"children":2644},{"id":2643},"_3-code-ownership",[2645],{"type":28,"value":2646},"3. Code Ownership",{"type":22,"tag":31,"props":2648,"children":2649},{},[2650],{"type":28,"value":2651},"Create a CODEOWNERS file:",{"type":22,"tag":133,"props":2653,"children":2655},{"code":2654},"# 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",[2656],{"type":22,"tag":87,"props":2657,"children":2658},{"__ignoreMap":7},[2659],{"type":28,"value":2654},{"type":22,"tag":23,"props":2661,"children":2663},{"id":2662},"performance-impact",[2664],{"type":28,"value":2665},"Performance Impact",{"type":22,"tag":31,"props":2667,"children":2668},{},[2669],{"type":28,"value":2670},"Monorepo with proper caching:",{"type":22,"tag":133,"props":2672,"children":2674},{"code":2673},"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",[2675],{"type":22,"tag":87,"props":2676,"children":2677},{"__ignoreMap":7},[2678],{"type":28,"value":2673},{"type":22,"tag":31,"props":2680,"children":2681},{},[2682],{"type":28,"value":2683},"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":22,"tag":23,"props":2685,"children":2687},{"id":2686},"team-autonomy-in-monorepo",[2688],{"type":28,"value":2689},"Team Autonomy in Monorepo",{"type":22,"tag":31,"props":2691,"children":2692},{},[2693],{"type":28,"value":2694},"To prevent chaos:",{"type":22,"tag":179,"props":2696,"children":2697},{},[2698,2708,2718,2736],{"type":22,"tag":46,"props":2699,"children":2700},{},[2701,2706],{"type":22,"tag":50,"props":2702,"children":2703},{},[2704],{"type":28,"value":2705},"Clear package boundaries",{"type":28,"value":2707}," — dashboard team doesn't touch billing code",{"type":22,"tag":46,"props":2709,"children":2710},{},[2711,2716],{"type":22,"tag":50,"props":2712,"children":2713},{},[2714],{"type":28,"value":2715},"Owned packages",{"type":28,"value":2717}," — each package has a code owner",{"type":22,"tag":46,"props":2719,"children":2720},{},[2721,2726,2728,2734],{"type":22,"tag":50,"props":2722,"children":2723},{},[2724],{"type":28,"value":2725},"Version contracts",{"type":28,"value":2727}," — ",{"type":22,"tag":87,"props":2729,"children":2731},{"className":2730},[],[2732],{"type":28,"value":2733},"@acme\u002Fapi-client",{"type":28,"value":2735}," v2.0.0 is stable, don't break it",{"type":22,"tag":46,"props":2737,"children":2738},{},[2739,2744],{"type":22,"tag":50,"props":2740,"children":2741},{},[2742],{"type":28,"value":2743},"Shared conventions",{"type":28,"value":2745}," — naming, folder structure, testing patterns",{"type":22,"tag":23,"props":2747,"children":2749},{"id":2748},"when-monorepo-isnt-enough",[2750],{"type":28,"value":2751},"When Monorepo Isn't Enough",{"type":22,"tag":31,"props":2753,"children":2754},{},[2755],{"type":28,"value":2756},"For teams that need complete independence, consider micro-frontends:",{"type":22,"tag":31,"props":2758,"children":2759},{},[2760,2765,2767,2772,2774,2779],{"type":22,"tag":50,"props":2761,"children":2762},{},[2763],{"type":28,"value":2764},"Monorepo",{"type":28,"value":2766}," → Single deployment, shared everything\n",{"type":22,"tag":50,"props":2768,"children":2769},{},[2770],{"type":28,"value":2771},"Micro-Frontends",{"type":28,"value":2773}," → Independent deployments, isolated teams\n",{"type":22,"tag":50,"props":2775,"children":2776},{},[2777],{"type":28,"value":2778},"Hybrid",{"type":28,"value":2780}," → Monorepo for shared packages, micro-frontends for apps",{"type":22,"tag":31,"props":2782,"children":2783},{},[2784],{"type":28,"value":2785},"We started with monorepo. If we grew to 200 engineers across 30 teams, we'd likely move to micro-frontends with Module Federation.",{"type":22,"tag":23,"props":2787,"children":2789},{"id":2788},"final-metrics",[2790],{"type":28,"value":2791},"Final Metrics",{"type":22,"tag":31,"props":2793,"children":2794},{},[2795],{"type":28,"value":2796},"The movement a monorepo migration typically produces (illustrative example — not measured claims from one project):",{"type":22,"tag":1376,"props":2798,"children":2799},{},[2800,2815],{"type":22,"tag":1380,"props":2801,"children":2802},{},[2803],{"type":22,"tag":1384,"props":2804,"children":2805},{},[2806,2810],{"type":22,"tag":1388,"props":2807,"children":2808},{},[2809],{"type":28,"value":1392},{"type":22,"tag":1388,"props":2811,"children":2812},{},[2813],{"type":28,"value":2814},"Direction",{"type":22,"tag":1409,"props":2816,"children":2817},{},[2818,2831,2844,2856,2869,2882],{"type":22,"tag":1384,"props":2819,"children":2820},{},[2821,2826],{"type":22,"tag":1416,"props":2822,"children":2823},{},[2824],{"type":28,"value":2825},"Build time",{"type":22,"tag":1416,"props":2827,"children":2828},{},[2829],{"type":28,"value":2830},"Sharply down with caching",{"type":22,"tag":1384,"props":2832,"children":2833},{},[2834,2839],{"type":22,"tag":1416,"props":2835,"children":2836},{},[2837],{"type":28,"value":2838},"CI\u002FCD time",{"type":22,"tag":1416,"props":2840,"children":2841},{},[2842],{"type":28,"value":2843},"Down, especially on unchanged packages",{"type":22,"tag":1384,"props":2845,"children":2846},{},[2847,2851],{"type":22,"tag":1416,"props":2848,"children":2849},{},[2850],{"type":28,"value":2150},{"type":22,"tag":1416,"props":2852,"children":2853},{},[2854],{"type":28,"value":2855},"Down — one repo, one integration point",{"type":22,"tag":1384,"props":2857,"children":2858},{},[2859,2864],{"type":22,"tag":1416,"props":2860,"children":2861},{},[2862],{"type":28,"value":2863},"Deployment failures",{"type":22,"tag":1416,"props":2865,"children":2866},{},[2867],{"type":28,"value":2868},"Down — shared tooling, fewer bespoke pipelines",{"type":22,"tag":1384,"props":2870,"children":2871},{},[2872,2877],{"type":22,"tag":1416,"props":2873,"children":2874},{},[2875],{"type":28,"value":2876},"Code duplication",{"type":22,"tag":1416,"props":2878,"children":2879},{},[2880],{"type":28,"value":2881},"Down — importing beats re-implementing",{"type":22,"tag":1384,"props":2883,"children":2884},{},[2885,2890],{"type":22,"tag":1416,"props":2886,"children":2887},{},[2888],{"type":28,"value":2889},"Onboarding time",{"type":22,"tag":1416,"props":2891,"children":2892},{},[2893],{"type":28,"value":2894},"Down — one setup, one set of conventions",{"type":22,"tag":31,"props":2896,"children":2897},{},[2898],{"type":28,"value":2899},"The architecture change pays off through clear ownership, faster development, and fewer bugs — if you enforce the boundaries.",{"title":7,"searchDepth":551,"depth":551,"links":2901},[2902,2903,2907,2910,2911,2912,2913,2918,2919,2920,2921],{"id":2131,"depth":551,"text":2134},{"id":2200,"depth":551,"text":2203,"children":2904},[2905,2906],{"id":2219,"depth":557,"text":2222},{"id":2278,"depth":557,"text":2281},{"id":2332,"depth":551,"text":2335,"children":2908},[2909],{"id":2379,"depth":557,"text":2382},{"id":2443,"depth":551,"text":2446},{"id":2507,"depth":551,"text":2510},{"id":2574,"depth":551,"text":2577},{"id":2594,"depth":551,"text":2597,"children":2914},[2915,2916,2917],{"id":2600,"depth":557,"text":2603},{"id":2628,"depth":557,"text":2631},{"id":2643,"depth":557,"text":2646},{"id":2662,"depth":551,"text":2665},{"id":2686,"depth":551,"text":2689},{"id":2748,"depth":551,"text":2751},{"id":2788,"depth":551,"text":2791},"content:blog:frontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas.md","blog\u002Ffrontend-architecture-for-large-saas",{"_path":2926,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":2927,"description":2928,"date":2929,"readingTime":2930,"tags":2931,"featured":6,"body":2932,"_type":564,"_id":3593,"_source":566,"_file":3594,"_stem":3595,"_extension":569},"\u002Fblog\u002Fcore-web-vitals-nuxt-guide","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,[16,1002],{"type":19,"children":2933,"toc":3567},[2934,2940,2945,3022,3028,3034,3042,3047,3053,3058,3067,3072,3081,3086,3095,3101,3106,3112,3117,3126,3131,3149,3155,3164,3170,3175,3184,3190,3195,3204,3210,3215,3221,3230,3236,3245,3251,3260,3266,3271,3319,3325,3334,3340,3349,3355,3364,3370,3375,3471,3477,3482,3491,3496,3505,3509,3562],{"type":22,"tag":23,"props":2935,"children":2937},{"id":2936},"understanding-core-web-vitals",[2938],{"type":28,"value":2939},"Understanding Core Web Vitals",{"type":22,"tag":31,"props":2941,"children":2942},{},[2943],{"type":28,"value":2944},"Google's Core Web Vitals measure three aspects of user experience:",{"type":22,"tag":179,"props":2946,"children":2947},{},[2948,2971,2999],{"type":22,"tag":46,"props":2949,"children":2950},{},[2951,2956,2958],{"type":22,"tag":50,"props":2952,"children":2953},{},[2954],{"type":28,"value":2955},"LCP (Largest Contentful Paint)",{"type":28,"value":2957}," — How fast does content appear?",{"type":22,"tag":42,"props":2959,"children":2960},{},[2961,2966],{"type":22,"tag":46,"props":2962,"children":2963},{},[2964],{"type":28,"value":2965},"Target: \u003C 2.5s",{"type":22,"tag":46,"props":2967,"children":2968},{},[2969],{"type":28,"value":2970},"Measures: When largest element (image, heading, video) becomes visible",{"type":22,"tag":46,"props":2972,"children":2973},{},[2974,2979,2981],{"type":22,"tag":50,"props":2975,"children":2976},{},[2977],{"type":28,"value":2978},"FID (First Input Delay)",{"type":28,"value":2980}," — How responsive is the page?",{"type":22,"tag":42,"props":2982,"children":2983},{},[2984,2989,2994],{"type":22,"tag":46,"props":2985,"children":2986},{},[2987],{"type":28,"value":2988},"Target: \u003C 100ms",{"type":22,"tag":46,"props":2990,"children":2991},{},[2992],{"type":28,"value":2993},"Measures: Delay from user input to browser processing",{"type":22,"tag":46,"props":2995,"children":2996},{},[2997],{"type":28,"value":2998},"Being replaced by INP in 2024",{"type":22,"tag":46,"props":3000,"children":3001},{},[3002,3007,3009],{"type":22,"tag":50,"props":3003,"children":3004},{},[3005],{"type":28,"value":3006},"CLS (Cumulative Layout Shift)",{"type":28,"value":3008}," — How stable is the layout?",{"type":22,"tag":42,"props":3010,"children":3011},{},[3012,3017],{"type":22,"tag":46,"props":3013,"children":3014},{},[3015],{"type":28,"value":3016},"Target: \u003C 0.1",{"type":22,"tag":46,"props":3018,"children":3019},{},[3020],{"type":28,"value":3021},"Measures: Unexpected visual shifts during page load",{"type":22,"tag":23,"props":3023,"children":3025},{"id":3024},"measuring-web-vitals",[3026],{"type":28,"value":3027},"Measuring Web Vitals",{"type":22,"tag":107,"props":3029,"children":3031},{"id":3030},"_1-google-pagespeed-insights",[3032],{"type":28,"value":3033},"1. Google PageSpeed Insights",{"type":22,"tag":133,"props":3035,"children":3037},{"code":3036},"https:\u002F\u002Fpagespeed.web.dev\u002F?url=yoursite.com\n",[3038],{"type":22,"tag":87,"props":3039,"children":3040},{"__ignoreMap":7},[3041],{"type":28,"value":3036},{"type":22,"tag":31,"props":3043,"children":3044},{},[3045],{"type":28,"value":3046},"Shows real-world data from actual users. This is what matters most.",{"type":22,"tag":107,"props":3048,"children":3050},{"id":3049},"_2-local-testing",[3051],{"type":28,"value":3052},"2. Local Testing",{"type":22,"tag":31,"props":3054,"children":3055},{},[3056],{"type":28,"value":3057},"Install web-vitals library:",{"type":22,"tag":133,"props":3059,"children":3062},{"code":3060,"language":1059,"meta":7,"className":3061},"npm install web-vitals\n",[1057],[3063],{"type":22,"tag":87,"props":3064,"children":3065},{"__ignoreMap":7},[3066],{"type":28,"value":3060},{"type":22,"tag":31,"props":3068,"children":3069},{},[3070],{"type":28,"value":3071},"Track metrics in Nuxt:",{"type":22,"tag":133,"props":3073,"children":3076},{"code":3074,"language":138,"meta":7,"className":3075},"\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",[136],[3077],{"type":22,"tag":87,"props":3078,"children":3079},{"__ignoreMap":7},[3080],{"type":28,"value":3074},{"type":22,"tag":31,"props":3082,"children":3083},{},[3084],{"type":28,"value":3085},"Send to analytics:",{"type":22,"tag":133,"props":3087,"children":3090},{"code":3088,"language":138,"meta":7,"className":3089},"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",[136],[3091],{"type":22,"tag":87,"props":3092,"children":3093},{"__ignoreMap":7},[3094],{"type":28,"value":3088},{"type":22,"tag":23,"props":3096,"children":3098},{"id":3097},"optimizing-lcp",[3099],{"type":28,"value":3100},"Optimizing LCP",{"type":22,"tag":31,"props":3102,"children":3103},{},[3104],{"type":28,"value":3105},"LCP is usually the main blocker. Focus here first.",{"type":22,"tag":107,"props":3107,"children":3109},{"id":3108},"strategy-1-optimize-largest-element",[3110],{"type":28,"value":3111},"Strategy 1: Optimize Largest Element",{"type":22,"tag":31,"props":3113,"children":3114},{},[3115],{"type":28,"value":3116},"Identify what's causing slow LCP:",{"type":22,"tag":133,"props":3118,"children":3121},{"code":3119,"language":138,"meta":7,"className":3120},"\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",[136],[3122],{"type":22,"tag":87,"props":3123,"children":3124},{"__ignoreMap":7},[3125],{"type":28,"value":3119},{"type":22,"tag":31,"props":3127,"children":3128},{},[3129],{"type":28,"value":3130},"Usually the largest element is:",{"type":22,"tag":42,"props":3132,"children":3133},{},[3134,3139,3144],{"type":22,"tag":46,"props":3135,"children":3136},{},[3137],{"type":28,"value":3138},"Hero image",{"type":22,"tag":46,"props":3140,"children":3141},{},[3142],{"type":28,"value":3143},"Main heading",{"type":22,"tag":46,"props":3145,"children":3146},{},[3147],{"type":28,"value":3148},"Large text block",{"type":22,"tag":107,"props":3150,"children":3152},{"id":3151},"strategy-2-preload-critical-resources",[3153],{"type":28,"value":3154},"Strategy 2: Preload Critical Resources",{"type":22,"tag":133,"props":3156,"children":3159},{"code":3157,"language":138,"meta":7,"className":3158},"\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",[136],[3160],{"type":22,"tag":87,"props":3161,"children":3162},{"__ignoreMap":7},[3163],{"type":28,"value":3157},{"type":22,"tag":107,"props":3165,"children":3167},{"id":3166},"strategy-3-server-side-rendering",[3168],{"type":28,"value":3169},"Strategy 3: Server-Side Rendering",{"type":22,"tag":31,"props":3171,"children":3172},{},[3173],{"type":28,"value":3174},"SSR ensures content is in HTML, not dependent on JavaScript:",{"type":22,"tag":133,"props":3176,"children":3179},{"code":3177,"language":138,"meta":7,"className":3178},"\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",[136],[3180],{"type":22,"tag":87,"props":3181,"children":3182},{"__ignoreMap":7},[3183],{"type":28,"value":3177},{"type":22,"tag":107,"props":3185,"children":3187},{"id":3186},"strategy-4-image-optimization",[3188],{"type":28,"value":3189},"Strategy 4: Image Optimization",{"type":22,"tag":31,"props":3191,"children":3192},{},[3193],{"type":28,"value":3194},"Hero images often block LCP. Optimize aggressively:",{"type":22,"tag":133,"props":3196,"children":3199},{"code":3197,"language":1167,"meta":7,"className":3198},"\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",[1165],[3200],{"type":22,"tag":87,"props":3201,"children":3202},{"__ignoreMap":7},[3203],{"type":28,"value":3197},{"type":22,"tag":23,"props":3205,"children":3207},{"id":3206},"optimizing-fid-and-inp",[3208],{"type":28,"value":3209},"Optimizing FID (and INP)",{"type":22,"tag":31,"props":3211,"children":3212},{},[3213],{"type":28,"value":3214},"FID measures JavaScript execution blocking input. Reduce with:",{"type":22,"tag":107,"props":3216,"children":3218},{"id":3217},"_1-code-splitting",[3219],{"type":28,"value":3220},"1. Code Splitting",{"type":22,"tag":133,"props":3222,"children":3225},{"code":3223,"language":138,"meta":7,"className":3224},"\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",[136],[3226],{"type":22,"tag":87,"props":3227,"children":3228},{"__ignoreMap":7},[3229],{"type":28,"value":3223},{"type":22,"tag":107,"props":3231,"children":3233},{"id":3232},"_2-web-workers-for-heavy-tasks",[3234],{"type":28,"value":3235},"2. Web Workers for Heavy Tasks",{"type":22,"tag":133,"props":3237,"children":3240},{"code":3238,"language":138,"meta":7,"className":3239},"\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",[136],[3241],{"type":22,"tag":87,"props":3242,"children":3243},{"__ignoreMap":7},[3244],{"type":28,"value":3238},{"type":22,"tag":107,"props":3246,"children":3248},{"id":3247},"_3-defer-non-critical-javascript",[3249],{"type":28,"value":3250},"3. Defer Non-Critical JavaScript",{"type":22,"tag":133,"props":3252,"children":3255},{"code":3253,"language":1167,"meta":7,"className":3254},"\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",[1165],[3256],{"type":22,"tag":87,"props":3257,"children":3258},{"__ignoreMap":7},[3259],{"type":28,"value":3253},{"type":22,"tag":23,"props":3261,"children":3263},{"id":3262},"optimizing-cls",[3264],{"type":28,"value":3265},"Optimizing CLS",{"type":22,"tag":31,"props":3267,"children":3268},{},[3269],{"type":28,"value":3270},"CLS is layout instability. Causes:",{"type":22,"tag":179,"props":3272,"children":3273},{},[3274,3284,3294,3309],{"type":22,"tag":46,"props":3275,"children":3276},{},[3277,3282],{"type":22,"tag":50,"props":3278,"children":3279},{},[3280],{"type":28,"value":3281},"Images without dimensions",{"type":28,"value":3283}," — add width\u002Fheight",{"type":22,"tag":46,"props":3285,"children":3286},{},[3287,3292],{"type":22,"tag":50,"props":3288,"children":3289},{},[3290],{"type":28,"value":3291},"Ads loading late",{"type":28,"value":3293}," — reserve space",{"type":22,"tag":46,"props":3295,"children":3296},{},[3297,3302,3304],{"type":22,"tag":50,"props":3298,"children":3299},{},[3300],{"type":28,"value":3301},"Fonts loading",{"type":28,"value":3303}," — use ",{"type":22,"tag":87,"props":3305,"children":3307},{"className":3306},[],[3308],{"type":28,"value":1273},{"type":22,"tag":46,"props":3310,"children":3311},{},[3312,3317],{"type":22,"tag":50,"props":3313,"children":3314},{},[3315],{"type":28,"value":3316},"Dynamic content",{"type":28,"value":3318}," — skeleton screens",{"type":22,"tag":107,"props":3320,"children":3322},{"id":3321},"fix-1-image-dimensions",[3323],{"type":28,"value":3324},"Fix 1: Image Dimensions",{"type":22,"tag":133,"props":3326,"children":3329},{"code":3327,"language":1167,"meta":7,"className":3328},"\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",[1165],[3330],{"type":22,"tag":87,"props":3331,"children":3332},{"__ignoreMap":7},[3333],{"type":28,"value":3327},{"type":22,"tag":107,"props":3335,"children":3337},{"id":3336},"fix-2-font-loading",[3338],{"type":28,"value":3339},"Fix 2: Font Loading",{"type":22,"tag":133,"props":3341,"children":3344},{"code":3342,"language":138,"meta":7,"className":3343},"\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",[136],[3345],{"type":22,"tag":87,"props":3346,"children":3347},{"__ignoreMap":7},[3348],{"type":28,"value":3342},{"type":22,"tag":107,"props":3350,"children":3352},{"id":3351},"fix-3-skeleton-screens",[3353],{"type":28,"value":3354},"Fix 3: Skeleton Screens",{"type":22,"tag":133,"props":3356,"children":3359},{"code":3357,"language":1167,"meta":7,"className":3358},"\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",[1165],[3360],{"type":22,"tag":87,"props":3361,"children":3362},{"__ignoreMap":7},[3363],{"type":28,"value":3357},{"type":22,"tag":23,"props":3365,"children":3367},{"id":3366},"real-world-results",[3368],{"type":28,"value":3369},"Real-World Results",{"type":22,"tag":31,"props":3371,"children":3372},{},[3373],{"type":28,"value":3374},"After implementing these optimizations:",{"type":22,"tag":1376,"props":3376,"children":3377},{},[3378,3396],{"type":22,"tag":1380,"props":3379,"children":3380},{},[3381],{"type":22,"tag":1384,"props":3382,"children":3383},{},[3384,3388,3392],{"type":22,"tag":1388,"props":3385,"children":3386},{},[3387],{"type":28,"value":1392},{"type":22,"tag":1388,"props":3389,"children":3390},{},[3391],{"type":28,"value":1397},{"type":22,"tag":1388,"props":3393,"children":3394},{},[3395],{"type":28,"value":1402},{"type":22,"tag":1409,"props":3397,"children":3398},{},[3399,3417,3435,3453],{"type":22,"tag":1384,"props":3400,"children":3401},{},[3402,3407,3412],{"type":22,"tag":1416,"props":3403,"children":3404},{},[3405],{"type":28,"value":3406},"LCP",{"type":22,"tag":1416,"props":3408,"children":3409},{},[3410],{"type":28,"value":3411},"3.8s",{"type":22,"tag":1416,"props":3413,"children":3414},{},[3415],{"type":28,"value":3416},"1.6s",{"type":22,"tag":1384,"props":3418,"children":3419},{},[3420,3425,3430],{"type":22,"tag":1416,"props":3421,"children":3422},{},[3423],{"type":28,"value":3424},"FID",{"type":22,"tag":1416,"props":3426,"children":3427},{},[3428],{"type":28,"value":3429},"180ms",{"type":22,"tag":1416,"props":3431,"children":3432},{},[3433],{"type":28,"value":3434},"45ms",{"type":22,"tag":1384,"props":3436,"children":3437},{},[3438,3443,3448],{"type":22,"tag":1416,"props":3439,"children":3440},{},[3441],{"type":28,"value":3442},"CLS",{"type":22,"tag":1416,"props":3444,"children":3445},{},[3446],{"type":28,"value":3447},"0.22",{"type":22,"tag":1416,"props":3449,"children":3450},{},[3451],{"type":28,"value":3452},"0.04",{"type":22,"tag":1384,"props":3454,"children":3455},{},[3456,3461,3466],{"type":22,"tag":1416,"props":3457,"children":3458},{},[3459],{"type":28,"value":3460},"PageSpeed Score",{"type":22,"tag":1416,"props":3462,"children":3463},{},[3464],{"type":28,"value":3465},"42",{"type":22,"tag":1416,"props":3467,"children":3468},{},[3469],{"type":28,"value":3470},"92",{"type":22,"tag":23,"props":3472,"children":3474},{"id":3473},"continuous-monitoring",[3475],{"type":28,"value":3476},"Continuous Monitoring",{"type":22,"tag":31,"props":3478,"children":3479},{},[3480],{"type":28,"value":3481},"Set up alerts for regressions:",{"type":22,"tag":133,"props":3483,"children":3486},{"code":3484,"language":138,"meta":7,"className":3485},"\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",[136],[3487],{"type":22,"tag":87,"props":3488,"children":3489},{"__ignoreMap":7},[3490],{"type":28,"value":3484},{"type":22,"tag":31,"props":3492,"children":3493},{},[3494],{"type":28,"value":3495},"Run in CI:",{"type":22,"tag":133,"props":3497,"children":3500},{"code":3498,"language":1059,"meta":7,"className":3499},"# .github\u002Fworkflows\u002Flighthouse.yml\n- name: Run Lighthouse CI\n  run: lhci autorun\n",[1057],[3501],{"type":22,"tag":87,"props":3502,"children":3503},{"__ignoreMap":7},[3504],{"type":28,"value":3498},{"type":22,"tag":23,"props":3506,"children":3507},{"id":1495},[3508],{"type":28,"value":1498},{"type":22,"tag":179,"props":3510,"children":3511},{},[3512,3522,3532,3542,3552],{"type":22,"tag":46,"props":3513,"children":3514},{},[3515,3520],{"type":22,"tag":50,"props":3516,"children":3517},{},[3518],{"type":28,"value":3519},"Measure real user data",{"type":28,"value":3521}," — lab data differs from production",{"type":22,"tag":46,"props":3523,"children":3524},{},[3525,3530],{"type":22,"tag":50,"props":3526,"children":3527},{},[3528],{"type":28,"value":3529},"LCP is usually the bottleneck",{"type":28,"value":3531}," — focus optimization efforts there",{"type":22,"tag":46,"props":3533,"children":3534},{},[3535,3540],{"type":22,"tag":50,"props":3536,"children":3537},{},[3538],{"type":28,"value":3539},"Images matter most",{"type":28,"value":3541}," — proper optimization yields biggest gains",{"type":22,"tag":46,"props":3543,"children":3544},{},[3545,3550],{"type":22,"tag":50,"props":3546,"children":3547},{},[3548],{"type":28,"value":3549},"Preload critical resources",{"type":28,"value":3551}," — hero images and fonts",{"type":22,"tag":46,"props":3553,"children":3554},{},[3555,3560],{"type":22,"tag":50,"props":3556,"children":3557},{},[3558],{"type":28,"value":3559},"Monitor continuously",{"type":28,"value":3561}," — regressions happen. Catch them early",{"type":22,"tag":31,"props":3563,"children":3564},{},[3565],{"type":28,"value":3566},"Core Web Vitals are no longer just SEO metrics—they're user experience fundamentals. Invest in getting them right.",{"title":7,"searchDepth":551,"depth":551,"links":3568},[3569,3570,3574,3580,3585,3590,3591,3592],{"id":2936,"depth":551,"text":2939},{"id":3024,"depth":551,"text":3027,"children":3571},[3572,3573],{"id":3030,"depth":557,"text":3033},{"id":3049,"depth":557,"text":3052},{"id":3097,"depth":551,"text":3100,"children":3575},[3576,3577,3578,3579],{"id":3108,"depth":557,"text":3111},{"id":3151,"depth":557,"text":3154},{"id":3166,"depth":557,"text":3169},{"id":3186,"depth":557,"text":3189},{"id":3206,"depth":551,"text":3209,"children":3581},[3582,3583,3584],{"id":3217,"depth":557,"text":3220},{"id":3232,"depth":557,"text":3235},{"id":3247,"depth":557,"text":3250},{"id":3262,"depth":551,"text":3265,"children":3586},[3587,3588,3589],{"id":3321,"depth":557,"text":3324},{"id":3336,"depth":557,"text":3339},{"id":3351,"depth":557,"text":3354},{"id":3366,"depth":551,"text":3369},{"id":3473,"depth":551,"text":3476},{"id":1495,"depth":551,"text":1498},"content:blog:core-web-vitals-nuxt-guide.md","blog\u002Fcore-web-vitals-nuxt-guide.md","blog\u002Fcore-web-vitals-nuxt-guide",{"_path":3597,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":3598,"description":3599,"date":3600,"updated":11,"readingTime":12,"tags":3601,"featured":6,"body":3604,"_type":564,"_id":4024,"_source":566,"_file":4025,"_stem":4026,"_extension":569},"\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",[3602,3603],"CI\u002FCD","DevOps",{"type":19,"children":3605,"toc":4008},[3606,3612,3617,3622,3635,3640,3645,3698,3704,3712,3718,3729,3735,3741,3746,3755,3760,3766,3775,3788,3794,3803,3808,3814,3823,3828,3856,3861,3867,3876,3881,3887,3896,3902,3907,3918,3924,3929,3934,3939,3944,3950,4003],{"type":22,"tag":23,"props":3607,"children":3609},{"id":3608},"the-state-of-frontend-cicd",[3610],{"type":28,"value":3611},"The State of Frontend CI\u002FCD",{"type":22,"tag":31,"props":3613,"children":3614},{},[3615],{"type":28,"value":3616},"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":22,"tag":31,"props":3618,"children":3619},{},[3620],{"type":28,"value":3621},"Many teams still use basic CI pipelines:",{"type":22,"tag":179,"props":3623,"children":3624},{},[3625,3630],{"type":22,"tag":46,"props":3626,"children":3627},{},[3628],{"type":28,"value":3629},"Run tests",{"type":22,"tag":46,"props":3631,"children":3632},{},[3633],{"type":28,"value":3634},"If pass, deploy",{"type":22,"tag":31,"props":3636,"children":3637},{},[3638],{"type":28,"value":3639},"Result: slow feedback, missed bugs, brittle deployments.",{"type":22,"tag":31,"props":3641,"children":3642},{},[3643],{"type":28,"value":3644},"Modern CI\u002FCD is sophisticated:",{"type":22,"tag":179,"props":3646,"children":3647},{},[3648,3658,3668,3678,3688],{"type":22,"tag":46,"props":3649,"children":3650},{},[3651,3656],{"type":22,"tag":50,"props":3652,"children":3653},{},[3654],{"type":28,"value":3655},"Parallel testing",{"type":28,"value":3657}," — unit, integration, e2e at the same time",{"type":22,"tag":46,"props":3659,"children":3660},{},[3661,3666],{"type":22,"tag":50,"props":3662,"children":3663},{},[3664],{"type":28,"value":3665},"Performance monitoring",{"type":28,"value":3667}," — reject PRs that slow down the app",{"type":22,"tag":46,"props":3669,"children":3670},{},[3671,3676],{"type":22,"tag":50,"props":3672,"children":3673},{},[3674],{"type":28,"value":3675},"Visual regression testing",{"type":28,"value":3677}," — catch unintended design changes",{"type":22,"tag":46,"props":3679,"children":3680},{},[3681,3686],{"type":22,"tag":50,"props":3682,"children":3683},{},[3684],{"type":28,"value":3685},"Semantic versioning",{"type":28,"value":3687}," — automate version bumps and releases",{"type":22,"tag":46,"props":3689,"children":3690},{},[3691,3696],{"type":22,"tag":50,"props":3692,"children":3693},{},[3694],{"type":28,"value":3695},"Multi-stage deployments",{"type":28,"value":3697}," — preview → staging → production",{"type":22,"tag":23,"props":3699,"children":3701},{"id":3700},"the-full-pipeline",[3702],{"type":28,"value":3703},"The Full Pipeline",{"type":22,"tag":133,"props":3705,"children":3707},{"code":3706},"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",[3708],{"type":22,"tag":87,"props":3709,"children":3710},{"__ignoreMap":7},[3711],{"type":28,"value":3706},{"type":22,"tag":23,"props":3713,"children":3715},{"id":3714},"github-actions-workflow",[3716],{"type":28,"value":3717},"GitHub Actions Workflow",{"type":22,"tag":133,"props":3719,"children":3724},{"code":3720,"language":3721,"meta":7,"className":3722},"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",[3723],"language-yaml",[3725],{"type":22,"tag":87,"props":3726,"children":3727},{"__ignoreMap":7},[3728],{"type":28,"value":3720},{"type":22,"tag":23,"props":3730,"children":3732},{"id":3731},"key-features-explained",[3733],{"type":28,"value":3734},"Key Features Explained",{"type":22,"tag":107,"props":3736,"children":3738},{"id":3737},"_1-parallel-job-execution",[3739],{"type":28,"value":3740},"1. Parallel Job Execution",{"type":22,"tag":31,"props":3742,"children":3743},{},[3744],{"type":28,"value":3745},"Tests run simultaneously instead of sequentially:",{"type":22,"tag":133,"props":3747,"children":3750},{"code":3748,"language":3721,"meta":7,"className":3749},"strategy:\n  matrix:\n    shard: [1, 2, 3, 4]\nrun: pnpm test -- --shard=${{ matrix.shard }}\u002F4\n",[3723],[3751],{"type":22,"tag":87,"props":3752,"children":3753},{"__ignoreMap":7},[3754],{"type":28,"value":3748},{"type":22,"tag":31,"props":3756,"children":3757},{},[3758],{"type":28,"value":3759},"Reduces CI time from 12 minutes → 4 minutes.",{"type":22,"tag":107,"props":3761,"children":3763},{"id":3762},"_2-dependency-caching",[3764],{"type":28,"value":3765},"2. Dependency Caching",{"type":22,"tag":133,"props":3767,"children":3770},{"code":3768,"language":3721,"meta":7,"className":3769},"- uses: actions\u002Fsetup-node@v3\n  with:\n    node-version: ${{ env.NODE_VERSION }}\n    cache: 'pnpm'\n",[3723],[3771],{"type":22,"tag":87,"props":3772,"children":3773},{"__ignoreMap":7},[3774],{"type":28,"value":3768},{"type":22,"tag":31,"props":3776,"children":3777},{},[3778,3780,3786],{"type":28,"value":3779},"Skips ",{"type":22,"tag":87,"props":3781,"children":3783},{"className":3782},[],[3784],{"type":28,"value":3785},"pnpm install",{"type":28,"value":3787}," if dependencies haven't changed. Saves 1-2 minutes per run.",{"type":22,"tag":107,"props":3789,"children":3791},{"id":3790},"_3-conditional-deployment",[3792],{"type":28,"value":3793},"3. Conditional Deployment",{"type":22,"tag":133,"props":3795,"children":3798},{"code":3796,"language":3721,"meta":7,"className":3797},"if: github.ref == 'refs\u002Fheads\u002Fmain' && github.event_name == 'push'\n",[3723],[3799],{"type":22,"tag":87,"props":3800,"children":3801},{"__ignoreMap":7},[3802],{"type":28,"value":3796},{"type":22,"tag":31,"props":3804,"children":3805},{},[3806],{"type":28,"value":3807},"Only deploy to production on main branch pushes. PRs get preview deployments.",{"type":22,"tag":107,"props":3809,"children":3811},{"id":3810},"_4-semantic-versioning",[3812],{"type":28,"value":3813},"4. Semantic Versioning",{"type":22,"tag":133,"props":3815,"children":3818},{"code":3816,"language":3721,"meta":7,"className":3817},"- uses: cycjimmy\u002Fsemantic-release-action@v3\n",[3723],[3819],{"type":22,"tag":87,"props":3820,"children":3821},{"__ignoreMap":7},[3822],{"type":28,"value":3816},{"type":22,"tag":31,"props":3824,"children":3825},{},[3826],{"type":28,"value":3827},"Automatically:",{"type":22,"tag":42,"props":3829,"children":3830},{},[3831,3836,3841,3846,3851],{"type":22,"tag":46,"props":3832,"children":3833},{},[3834],{"type":28,"value":3835},"Analyzes commits (feat → minor, fix → patch)",{"type":22,"tag":46,"props":3837,"children":3838},{},[3839],{"type":28,"value":3840},"Bumps version in package.json",{"type":22,"tag":46,"props":3842,"children":3843},{},[3844],{"type":28,"value":3845},"Creates GitHub release",{"type":22,"tag":46,"props":3847,"children":3848},{},[3849],{"type":28,"value":3850},"Publishes to npm",{"type":22,"tag":46,"props":3852,"children":3853},{},[3854],{"type":28,"value":3855},"Generates changelog",{"type":22,"tag":31,"props":3857,"children":3858},{},[3859],{"type":28,"value":3860},"One less manual step.",{"type":22,"tag":107,"props":3862,"children":3864},{"id":3863},"_5-performance-monitoring",[3865],{"type":28,"value":3866},"5. Performance Monitoring",{"type":22,"tag":133,"props":3868,"children":3871},{"code":3869,"language":3721,"meta":7,"className":3870},"- uses: treosh\u002Flighthouse-ci-action@v9\n",[3723],[3872],{"type":22,"tag":87,"props":3873,"children":3874},{"__ignoreMap":7},[3875],{"type":28,"value":3869},{"type":22,"tag":31,"props":3877,"children":3878},{},[3879],{"type":28,"value":3880},"Blocks deployment if performance degrades. Ensures Lighthouse score stays above threshold.",{"type":22,"tag":23,"props":3882,"children":3884},{"id":3883},"caching-strategy",[3885],{"type":28,"value":3886},"Caching Strategy",{"type":22,"tag":133,"props":3888,"children":3891},{"code":3889,"language":3721,"meta":7,"className":3890},"# .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",[3723],[3892],{"type":22,"tag":87,"props":3893,"children":3894},{"__ignoreMap":7},[3895],{"type":28,"value":3889},{"type":22,"tag":23,"props":3897,"children":3899},{"id":3898},"results",[3900],{"type":28,"value":3901},"Results",{"type":22,"tag":31,"props":3903,"children":3904},{},[3905],{"type":28,"value":3906},"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":22,"tag":31,"props":3908,"children":3909},{},[3910,3912,3917],{"type":28,"value":3911},"My verified production result from building this kind of pipeline at Ordant: ",{"type":22,"tag":50,"props":3913,"children":3914},{},[3915],{"type":28,"value":3916},"merge-to-deploy time down 40%, with 99.9% uptime",{"type":28,"value":1635},{"type":22,"tag":23,"props":3919,"children":3921},{"id":3920},"cost-optimization",[3922],{"type":28,"value":3923},"Cost Optimization",{"type":22,"tag":31,"props":3925,"children":3926},{},[3927],{"type":28,"value":3928},"Worked example at GitHub Actions list pricing ($0.008\u002Fminute per job):",{"type":22,"tag":31,"props":3930,"children":3931},{},[3932],{"type":28,"value":3933},"A 5-minute pipeline with 6 parallel jobs = 30 job-minutes = $0.24\u002Frun.",{"type":22,"tag":31,"props":3935,"children":3936},{},[3937],{"type":28,"value":3938},"At 30 runs\u002Fday, that's $7.20\u002Fday ≈ $216\u002Fmonth.",{"type":22,"tag":31,"props":3940,"children":3941},{},[3942],{"type":28,"value":3943},"Worth it for the bugs prevented and deployment speed gained.",{"type":22,"tag":23,"props":3945,"children":3947},{"id":3946},"pro-tips",[3948],{"type":28,"value":3949},"Pro Tips",{"type":22,"tag":179,"props":3951,"children":3952},{},[3953,3963,3973,3983,3993],{"type":22,"tag":46,"props":3954,"children":3955},{},[3956,3961],{"type":22,"tag":50,"props":3957,"children":3958},{},[3959],{"type":28,"value":3960},"Cache aggressively",{"type":28,"value":3962}," — saves 1-3 minutes per run",{"type":22,"tag":46,"props":3964,"children":3965},{},[3966,3971],{"type":22,"tag":50,"props":3967,"children":3968},{},[3969],{"type":28,"value":3970},"Run tests in parallel",{"type":28,"value":3972}," — hardware is cheaper than time",{"type":22,"tag":46,"props":3974,"children":3975},{},[3976,3981],{"type":22,"tag":50,"props":3977,"children":3978},{},[3979],{"type":28,"value":3980},"Fail fast",{"type":28,"value":3982}," — lint before tests, tests before build",{"type":22,"tag":46,"props":3984,"children":3985},{},[3986,3991],{"type":22,"tag":50,"props":3987,"children":3988},{},[3989],{"type":28,"value":3990},"Monitor performance",{"type":28,"value":3992}," — performance regression is a deployment blocker",{"type":22,"tag":46,"props":3994,"children":3995},{},[3996,4001],{"type":22,"tag":50,"props":3997,"children":3998},{},[3999],{"type":28,"value":4000},"Automate versioning",{"type":28,"value":4002}," — semantic-release removes guesswork",{"type":22,"tag":31,"props":4004,"children":4005},{},[4006],{"type":28,"value":4007},"Good CI\u002FCD is a force multiplier. Invest in it early.",{"title":7,"searchDepth":551,"depth":551,"links":4009},[4010,4011,4012,4013,4020,4021,4022,4023],{"id":3608,"depth":551,"text":3611},{"id":3700,"depth":551,"text":3703},{"id":3714,"depth":551,"text":3717},{"id":3731,"depth":551,"text":3734,"children":4014},[4015,4016,4017,4018,4019],{"id":3737,"depth":557,"text":3740},{"id":3762,"depth":557,"text":3765},{"id":3790,"depth":557,"text":3793},{"id":3810,"depth":557,"text":3813},{"id":3863,"depth":557,"text":3866},{"id":3883,"depth":551,"text":3886},{"id":3898,"depth":551,"text":3901},{"id":3920,"depth":551,"text":3923},{"id":3946,"depth":551,"text":3949},"content:blog:advanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows.md","blog\u002Fadvanced-frontend-ci-cd-workflows",{"_path":4028,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":4029,"description":4030,"date":4031,"readingTime":1580,"tags":4032,"featured":6,"body":4034,"_type":564,"_id":4883,"_source":566,"_file":4884,"_stem":4885,"_extension":569},"\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",[4033,14],"Accessibility",{"type":19,"children":4035,"toc":4870},[4036,4042,4047,4052,4095,4100,4106,4111,4144,4155,4161,4166,4175,4180,4295,4301,4306,4315,4320,4400,4406,4411,4420,4426,4431,4440,4445,4463,4469,4474,4483,4488,4540,4546,4555,4560,4589,4595,4600,4609,4618,4623,4632,4638,4643,4831,4837,4842,4865],{"type":22,"tag":23,"props":4037,"children":4039},{"id":4038},"why-accessibility-matters",[4040],{"type":28,"value":4041},"Why Accessibility Matters",{"type":22,"tag":31,"props":4043,"children":4044},{},[4045],{"type":28,"value":4046},"15% of the global population has disabilities. That's 1 billion people.",{"type":22,"tag":31,"props":4048,"children":4049},{},[4050],{"type":28,"value":4051},"For businesses:",{"type":22,"tag":42,"props":4053,"children":4054},{},[4055,4065,4075,4085],{"type":22,"tag":46,"props":4056,"children":4057},{},[4058,4063],{"type":22,"tag":50,"props":4059,"children":4060},{},[4061],{"type":28,"value":4062},"Legal",{"type":28,"value":4064},": WCAG compliance required in many jurisdictions (US, UK, EU)",{"type":22,"tag":46,"props":4066,"children":4067},{},[4068,4073],{"type":22,"tag":50,"props":4069,"children":4070},{},[4071],{"type":28,"value":4072},"Market",{"type":28,"value":4074},": Accessible sites reach 1 billion+ users",{"type":22,"tag":46,"props":4076,"children":4077},{},[4078,4083],{"type":22,"tag":50,"props":4079,"children":4080},{},[4081],{"type":28,"value":4082},"SEO",{"type":28,"value":4084},": Google rewards accessible sites",{"type":22,"tag":46,"props":4086,"children":4087},{},[4088,4093],{"type":22,"tag":50,"props":4089,"children":4090},{},[4091],{"type":28,"value":4092},"UX",{"type":28,"value":4094},": Good accessibility is good UX for everyone (captions help in noisy environments)",{"type":22,"tag":31,"props":4096,"children":4097},{},[4098],{"type":28,"value":4099},"Accessibility isn't a feature. It's a requirement.",{"type":22,"tag":23,"props":4101,"children":4103},{"id":4102},"web-content-accessibility-guidelines-wcag-21",[4104],{"type":28,"value":4105},"Web Content Accessibility Guidelines (WCAG) 2.1",{"type":22,"tag":31,"props":4107,"children":4108},{},[4109],{"type":28,"value":4110},"The standard has three levels:",{"type":22,"tag":42,"props":4112,"children":4113},{},[4114,4124,4134],{"type":22,"tag":46,"props":4115,"children":4116},{},[4117,4122],{"type":22,"tag":50,"props":4118,"children":4119},{},[4120],{"type":28,"value":4121},"A",{"type":28,"value":4123},": Minimum compliance",{"type":22,"tag":46,"props":4125,"children":4126},{},[4127,4132],{"type":22,"tag":50,"props":4128,"children":4129},{},[4130],{"type":28,"value":4131},"AA",{"type":28,"value":4133},": Recommended (what most sites aim for)",{"type":22,"tag":46,"props":4135,"children":4136},{},[4137,4142],{"type":22,"tag":50,"props":4138,"children":4139},{},[4140],{"type":28,"value":4141},"AAA",{"type":28,"value":4143},": Enhanced (nice to have)",{"type":22,"tag":31,"props":4145,"children":4146},{},[4147,4149,4153],{"type":28,"value":4148},"We'll focus on ",{"type":22,"tag":50,"props":4150,"children":4151},{},[4152],{"type":28,"value":4131},{"type":28,"value":4154},", the legal standard in most places.",{"type":22,"tag":23,"props":4156,"children":4158},{"id":4157},"semantic-html",[4159],{"type":28,"value":4160},"Semantic HTML",{"type":22,"tag":31,"props":4162,"children":4163},{},[4164],{"type":28,"value":4165},"The foundation of accessibility:",{"type":22,"tag":133,"props":4167,"children":4170},{"className":4168,"code":4169,"language":1167,"meta":7},[1165],"\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",[4171],{"type":22,"tag":87,"props":4172,"children":4173},{"__ignoreMap":7},[4174],{"type":28,"value":4169},{"type":22,"tag":31,"props":4176,"children":4177},{},[4178],{"type":28,"value":4179},"Semantic elements:",{"type":22,"tag":42,"props":4181,"children":4182},{},[4183,4228,4247,4258,4269],{"type":22,"tag":46,"props":4184,"children":4185},{},[4186,4192,4194,4200,4201,4207,4208,4214,4215,4221,4222],{"type":22,"tag":87,"props":4187,"children":4189},{"className":4188},[],[4190],{"type":28,"value":4191},"\u003Cheader>",{"type":28,"value":4193},", ",{"type":22,"tag":87,"props":4195,"children":4197},{"className":4196},[],[4198],{"type":28,"value":4199},"\u003Cnav>",{"type":28,"value":4193},{"type":22,"tag":87,"props":4202,"children":4204},{"className":4203},[],[4205],{"type":28,"value":4206},"\u003Cmain>",{"type":28,"value":4193},{"type":22,"tag":87,"props":4209,"children":4211},{"className":4210},[],[4212],{"type":28,"value":4213},"\u003Carticle>",{"type":28,"value":4193},{"type":22,"tag":87,"props":4216,"children":4218},{"className":4217},[],[4219],{"type":28,"value":4220},"\u003Csection>",{"type":28,"value":4193},{"type":22,"tag":87,"props":4223,"children":4225},{"className":4224},[],[4226],{"type":28,"value":4227},"\u003Cfooter>",{"type":22,"tag":46,"props":4229,"children":4230},{},[4231,4237,4239,4245],{"type":22,"tag":87,"props":4232,"children":4234},{"className":4233},[],[4235],{"type":28,"value":4236},"\u003Ch1>",{"type":28,"value":4238}," - ",{"type":22,"tag":87,"props":4240,"children":4242},{"className":4241},[],[4243],{"type":28,"value":4244},"\u003Ch6>",{"type":28,"value":4246}," for headings (never skip levels)",{"type":22,"tag":46,"props":4248,"children":4249},{},[4250,4256],{"type":22,"tag":87,"props":4251,"children":4253},{"className":4252},[],[4254],{"type":28,"value":4255},"\u003Cbutton>",{"type":28,"value":4257}," for interactive elements",{"type":22,"tag":46,"props":4259,"children":4260},{},[4261,4267],{"type":22,"tag":87,"props":4262,"children":4264},{"className":4263},[],[4265],{"type":28,"value":4266},"\u003Clabel>",{"type":28,"value":4268}," for form inputs",{"type":22,"tag":46,"props":4270,"children":4271},{},[4272,4278,4280,4286,4287,4293],{"type":22,"tag":87,"props":4273,"children":4275},{"className":4274},[],[4276],{"type":28,"value":4277},"\u003Ctable>",{"type":28,"value":4279}," for tabular data (with ",{"type":22,"tag":87,"props":4281,"children":4283},{"className":4282},[],[4284],{"type":28,"value":4285},"\u003Cthead>",{"type":28,"value":4193},{"type":22,"tag":87,"props":4288,"children":4290},{"className":4289},[],[4291],{"type":28,"value":4292},"\u003Ctbody>",{"type":28,"value":4294},")",{"type":22,"tag":23,"props":4296,"children":4298},{"id":4297},"aria-accessible-rich-internet-applications",[4299],{"type":28,"value":4300},"ARIA (Accessible Rich Internet Applications)",{"type":22,"tag":31,"props":4302,"children":4303},{},[4304],{"type":28,"value":4305},"Use ARIA when HTML alone can't describe the UI:",{"type":22,"tag":133,"props":4307,"children":4310},{"className":4308,"code":4309,"language":1167,"meta":7},[1165],"\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",[4311],{"type":22,"tag":87,"props":4312,"children":4313},{"__ignoreMap":7},[4314],{"type":28,"value":4309},{"type":22,"tag":31,"props":4316,"children":4317},{},[4318],{"type":28,"value":4319},"Key ARIA attributes:",{"type":22,"tag":42,"props":4321,"children":4322},{},[4323,4334,4345,4356,4367,4378,4389],{"type":22,"tag":46,"props":4324,"children":4325},{},[4326,4332],{"type":22,"tag":87,"props":4327,"children":4329},{"className":4328},[],[4330],{"type":28,"value":4331},"aria-label",{"type":28,"value":4333}," — Label for screen readers",{"type":22,"tag":46,"props":4335,"children":4336},{},[4337,4343],{"type":22,"tag":87,"props":4338,"children":4340},{"className":4339},[],[4341],{"type":28,"value":4342},"aria-labelledby",{"type":28,"value":4344}," — Reference element that labels this one",{"type":22,"tag":46,"props":4346,"children":4347},{},[4348,4354],{"type":22,"tag":87,"props":4349,"children":4351},{"className":4350},[],[4352],{"type":28,"value":4353},"aria-describedby",{"type":28,"value":4355}," — Reference element that describes this one",{"type":22,"tag":46,"props":4357,"children":4358},{},[4359,4365],{"type":22,"tag":87,"props":4360,"children":4362},{"className":4361},[],[4363],{"type":28,"value":4364},"aria-expanded",{"type":28,"value":4366}," — Whether collapsible element is open",{"type":22,"tag":46,"props":4368,"children":4369},{},[4370,4376],{"type":22,"tag":87,"props":4371,"children":4373},{"className":4372},[],[4374],{"type":28,"value":4375},"aria-hidden",{"type":28,"value":4377}," — Hide from screen readers (for decorative elements)",{"type":22,"tag":46,"props":4379,"children":4380},{},[4381,4387],{"type":22,"tag":87,"props":4382,"children":4384},{"className":4383},[],[4385],{"type":28,"value":4386},"aria-live",{"type":28,"value":4388}," — Announce changes dynamically",{"type":22,"tag":46,"props":4390,"children":4391},{},[4392,4398],{"type":22,"tag":87,"props":4393,"children":4395},{"className":4394},[],[4396],{"type":28,"value":4397},"role",{"type":28,"value":4399}," — Define element purpose when semantic HTML won't work",{"type":22,"tag":23,"props":4401,"children":4403},{"id":4402},"keyboard-navigation",[4404],{"type":28,"value":4405},"Keyboard Navigation",{"type":22,"tag":31,"props":4407,"children":4408},{},[4409],{"type":28,"value":4410},"Many users navigate by keyboard only. Every interactive element must be keyboard accessible:",{"type":22,"tag":133,"props":4412,"children":4415},{"className":4413,"code":4414,"language":1167,"meta":7},[1165],"\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",[4416],{"type":22,"tag":87,"props":4417,"children":4418},{"__ignoreMap":7},[4419],{"type":28,"value":4414},{"type":22,"tag":23,"props":4421,"children":4423},{"id":4422},"color-contrast",[4424],{"type":28,"value":4425},"Color Contrast",{"type":22,"tag":31,"props":4427,"children":4428},{},[4429],{"type":28,"value":4430},"Text must have sufficient contrast ratio:",{"type":22,"tag":133,"props":4432,"children":4435},{"className":4433,"code":4434,"language":1167,"meta":7},[1165],"\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",[4436],{"type":22,"tag":87,"props":4437,"children":4438},{"__ignoreMap":7},[4439],{"type":28,"value":4434},{"type":22,"tag":31,"props":4441,"children":4442},{},[4443],{"type":28,"value":4444},"Use tools to check:",{"type":22,"tag":42,"props":4446,"children":4447},{},[4448,4453,4458],{"type":22,"tag":46,"props":4449,"children":4450},{},[4451],{"type":28,"value":4452},"WebAIM Contrast Checker",{"type":22,"tag":46,"props":4454,"children":4455},{},[4456],{"type":28,"value":4457},"Accessible Colors",{"type":22,"tag":46,"props":4459,"children":4460},{},[4461],{"type":28,"value":4462},"Chrome DevTools (automatic detection)",{"type":22,"tag":23,"props":4464,"children":4466},{"id":4465},"form-accessibility",[4467],{"type":28,"value":4468},"Form Accessibility",{"type":22,"tag":31,"props":4470,"children":4471},{},[4472],{"type":28,"value":4473},"Forms are a common accessibility fail point:",{"type":22,"tag":133,"props":4475,"children":4478},{"className":4476,"code":4477,"language":1167,"meta":7},[1165],"\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",[4479],{"type":22,"tag":87,"props":4480,"children":4481},{"__ignoreMap":7},[4482],{"type":28,"value":4477},{"type":22,"tag":31,"props":4484,"children":4485},{},[4486],{"type":28,"value":4487},"Key form patterns:",{"type":22,"tag":42,"props":4489,"children":4490},{},[4491,4501,4512,4522,4527],{"type":22,"tag":46,"props":4492,"children":4493},{},[4494,4496],{"type":28,"value":4495},"Every input has a ",{"type":22,"tag":87,"props":4497,"children":4499},{"className":4498},[],[4500],{"type":28,"value":4266},{"type":22,"tag":46,"props":4502,"children":4503},{},[4504,4506],{"type":28,"value":4505},"Labels are connected with ",{"type":22,"tag":87,"props":4507,"children":4509},{"className":4508},[],[4510],{"type":28,"value":4511},"for=\"id\"",{"type":22,"tag":46,"props":4513,"children":4514},{},[4515,4517],{"type":28,"value":4516},"Error messages use ",{"type":22,"tag":87,"props":4518,"children":4520},{"className":4519},[],[4521],{"type":28,"value":4353},{"type":22,"tag":46,"props":4523,"children":4524},{},[4525],{"type":28,"value":4526},"Fieldset groups related inputs",{"type":22,"tag":46,"props":4528,"children":4529},{},[4530,4532,4538],{"type":28,"value":4531},"Use ",{"type":22,"tag":87,"props":4533,"children":4535},{"className":4534},[],[4536],{"type":28,"value":4537},"aria-invalid",{"type":28,"value":4539}," for validation states",{"type":22,"tag":23,"props":4541,"children":4543},{"id":4542},"images-media",[4544],{"type":28,"value":4545},"Images & Media",{"type":22,"tag":133,"props":4547,"children":4550},{"className":4548,"code":4549,"language":1167,"meta":7},[1165],"\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",[4551],{"type":22,"tag":87,"props":4552,"children":4553},{"__ignoreMap":7},[4554],{"type":28,"value":4549},{"type":22,"tag":31,"props":4556,"children":4557},{},[4558],{"type":28,"value":4559},"Alt text guidelines:",{"type":22,"tag":42,"props":4561,"children":4562},{},[4563,4568,4579,4584],{"type":22,"tag":46,"props":4564,"children":4565},{},[4566],{"type":28,"value":4567},"Describe the content, not \"image of\"",{"type":22,"tag":46,"props":4569,"children":4570},{},[4571,4573],{"type":28,"value":4572},"Decorative images get ",{"type":22,"tag":87,"props":4574,"children":4576},{"className":4575},[],[4577],{"type":28,"value":4578},"alt=\"\"",{"type":22,"tag":46,"props":4580,"children":4581},{},[4582],{"type":28,"value":4583},"Icons need aria-label if no visible text",{"type":22,"tag":46,"props":4585,"children":4586},{},[4587],{"type":28,"value":4588},"Keep under 125 characters",{"type":22,"tag":23,"props":4590,"children":4592},{"id":4591},"automated-testing",[4593],{"type":28,"value":4594},"Automated Testing",{"type":22,"tag":31,"props":4596,"children":4597},{},[4598],{"type":28,"value":4599},"Use axe for automated accessibility testing:",{"type":22,"tag":133,"props":4601,"children":4604},{"className":4602,"code":4603,"language":1059,"meta":7},[1057],"npm install -D @axe-core\u002Fplaywright jest-axe\n",[4605],{"type":22,"tag":87,"props":4606,"children":4607},{"__ignoreMap":7},[4608],{"type":28,"value":4603},{"type":22,"tag":133,"props":4610,"children":4613},{"className":4611,"code":4612,"language":138,"meta":7},[136],"\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",[4614],{"type":22,"tag":87,"props":4615,"children":4616},{"__ignoreMap":7},[4617],{"type":28,"value":4612},{"type":22,"tag":31,"props":4619,"children":4620},{},[4621],{"type":28,"value":4622},"Jest testing:",{"type":22,"tag":133,"props":4624,"children":4627},{"className":4625,"code":4626,"language":138,"meta":7},[136],"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",[4628],{"type":22,"tag":87,"props":4629,"children":4630},{"__ignoreMap":7},[4631],{"type":28,"value":4626},{"type":22,"tag":23,"props":4633,"children":4635},{"id":4634},"accessibility-checklist",[4636],{"type":28,"value":4637},"Accessibility Checklist",{"type":22,"tag":31,"props":4639,"children":4640},{},[4641],{"type":28,"value":4642},"Before shipping:",{"type":22,"tag":42,"props":4644,"children":4647},{"className":4645},[4646],"contains-task-list",[4648,4666,4681,4696,4711,4726,4741,4756,4771,4786,4801,4816],{"type":22,"tag":46,"props":4649,"children":4652},{"className":4650},[4651],"task-list-item",[4653,4658,4660,4664],{"type":22,"tag":4654,"props":4655,"children":4657},"input",{"disabled":17,"type":4656},"checkbox",[],{"type":28,"value":4659}," ",{"type":22,"tag":50,"props":4661,"children":4662},{},[4663],{"type":28,"value":4160},{"type":28,"value":4665}," — use proper elements, not divs",{"type":22,"tag":46,"props":4667,"children":4669},{"className":4668},[4651],[4670,4673,4674,4679],{"type":22,"tag":4654,"props":4671,"children":4672},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4675,"children":4676},{},[4677],{"type":28,"value":4678},"Color contrast",{"type":28,"value":4680}," — 4.5:1 for normal text",{"type":22,"tag":46,"props":4682,"children":4684},{"className":4683},[4651],[4685,4688,4689,4694],{"type":22,"tag":4654,"props":4686,"children":4687},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4690,"children":4691},{},[4692],{"type":28,"value":4693},"Keyboard navigation",{"type":28,"value":4695}," — every interactive element accessible",{"type":22,"tag":46,"props":4697,"children":4699},{"className":4698},[4651],[4700,4703,4704,4709],{"type":22,"tag":4654,"props":4701,"children":4702},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4705,"children":4706},{},[4707],{"type":28,"value":4708},"Focus visible",{"type":28,"value":4710}," — clear focus indicator visible",{"type":22,"tag":46,"props":4712,"children":4714},{"className":4713},[4651],[4715,4718,4719,4724],{"type":22,"tag":4654,"props":4716,"children":4717},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4720,"children":4721},{},[4722],{"type":28,"value":4723},"Forms labeled",{"type":28,"value":4725}," — all inputs have labels",{"type":22,"tag":46,"props":4727,"children":4729},{"className":4728},[4651],[4730,4733,4734,4739],{"type":22,"tag":4654,"props":4731,"children":4732},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4735,"children":4736},{},[4737],{"type":28,"value":4738},"Images have alt text",{"type":28,"value":4740}," — descriptive, not vague",{"type":22,"tag":46,"props":4742,"children":4744},{"className":4743},[4651],[4745,4748,4749,4754],{"type":22,"tag":4654,"props":4746,"children":4747},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4750,"children":4751},{},[4752],{"type":28,"value":4753},"Headings logical",{"type":28,"value":4755}," — h1 → h2 → h3 (no skipping)",{"type":22,"tag":46,"props":4757,"children":4759},{"className":4758},[4651],[4760,4763,4764,4769],{"type":22,"tag":4654,"props":4761,"children":4762},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4765,"children":4766},{},[4767],{"type":28,"value":4768},"ARIA appropriate",{"type":28,"value":4770}," — only when necessary",{"type":22,"tag":46,"props":4772,"children":4774},{"className":4773},[4651],[4775,4778,4779,4784],{"type":22,"tag":4654,"props":4776,"children":4777},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4780,"children":4781},{},[4782],{"type":28,"value":4783},"No flash",{"type":28,"value":4785}," — no content flashing > 3 times\u002Fsecond",{"type":22,"tag":46,"props":4787,"children":4789},{"className":4788},[4651],[4790,4793,4794,4799],{"type":22,"tag":4654,"props":4791,"children":4792},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4795,"children":4796},{},[4797],{"type":28,"value":4798},"Responsive text",{"type":28,"value":4800}," — readable at 200% zoom",{"type":22,"tag":46,"props":4802,"children":4804},{"className":4803},[4651],[4805,4808,4809,4814],{"type":22,"tag":4654,"props":4806,"children":4807},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4810,"children":4811},{},[4812],{"type":28,"value":4813},"Mobile accessible",{"type":28,"value":4815}," — touch targets 48px minimum",{"type":22,"tag":46,"props":4817,"children":4819},{"className":4818},[4651],[4820,4823,4824,4829],{"type":22,"tag":4654,"props":4821,"children":4822},{"disabled":17,"type":4656},[],{"type":28,"value":4659},{"type":22,"tag":50,"props":4825,"children":4826},{},[4827],{"type":28,"value":4828},"Automated tests pass",{"type":28,"value":4830}," — axe, jest-axe",{"type":22,"tag":23,"props":4832,"children":4834},{"id":4833},"real-impact",[4835],{"type":28,"value":4836},"Real Impact",{"type":22,"tag":31,"props":4838,"children":4839},{},[4840],{"type":28,"value":4841},"After implementing accessibility:",{"type":22,"tag":42,"props":4843,"children":4844},{},[4845,4850,4855,4860],{"type":22,"tag":46,"props":4846,"children":4847},{},[4848],{"type":28,"value":4849},"Accessibility score: 40 → 92\u002F100 (Lighthouse)",{"type":22,"tag":46,"props":4851,"children":4852},{},[4853],{"type":28,"value":4854},"Users with assistive tech: +8% traffic",{"type":22,"tag":46,"props":4856,"children":4857},{},[4858],{"type":28,"value":4859},"SEO improvement: +15% in search ranking",{"type":22,"tag":46,"props":4861,"children":4862},{},[4863],{"type":28,"value":4864},"General UX improvement: better for everyone",{"type":22,"tag":31,"props":4866,"children":4867},{},[4868],{"type":28,"value":4869},"Accessibility isn't a nice-to-have. It's engineering excellence.",{"title":7,"searchDepth":551,"depth":551,"links":4871},[4872,4873,4874,4875,4876,4877,4878,4879,4880,4881,4882],{"id":4038,"depth":551,"text":4041},{"id":4102,"depth":551,"text":4105},{"id":4157,"depth":551,"text":4160},{"id":4297,"depth":551,"text":4300},{"id":4402,"depth":551,"text":4405},{"id":4422,"depth":551,"text":4425},{"id":4465,"depth":551,"text":4468},{"id":4542,"depth":551,"text":4545},{"id":4591,"depth":551,"text":4594},{"id":4634,"depth":551,"text":4637},{"id":4833,"depth":551,"text":4836},"content:blog:accessibility-engineering-in-vue.md","blog\u002Faccessibility-engineering-in-vue.md","blog\u002Faccessibility-engineering-in-vue",{"_path":4887,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":4888,"description":4889,"date":4890,"readingTime":2930,"tags":4891,"featured":6,"body":4892,"_type":564,"_id":5346,"_source":566,"_file":5347,"_stem":5348,"_extension":569},"\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",[15,14],{"type":19,"children":4893,"toc":5334},[4894,4900,4905,4915,4923,4928,4938,4946,4951,4961,4969,4974,4980,4992,5000,5004,5061,5067,5072,5081,5086,5095,5100,5109,5115,5120,5129,5134,5143,5149,5154,5163,5167,5176,5182,5187,5196,5201,5207,5216,5222,5227,5235,5241,5246,5254,5260,5329],{"type":22,"tag":23,"props":4895,"children":4897},{"id":4896},"the-folder-structure-problem",[4898],{"type":28,"value":4899},"The Folder Structure Problem",{"type":22,"tag":31,"props":4901,"children":4902},{},[4903],{"type":28,"value":4904},"I've seen three patterns:",{"type":22,"tag":31,"props":4906,"children":4907},{},[4908,4913],{"type":22,"tag":50,"props":4909,"children":4910},{},[4911],{"type":28,"value":4912},"Chaos Model",{"type":28,"value":4914}," (what startups do):",{"type":22,"tag":133,"props":4916,"children":4918},{"code":4917},"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",[4919],{"type":22,"tag":87,"props":4920,"children":4921},{"__ignoreMap":7},[4922],{"type":28,"value":4917},{"type":22,"tag":31,"props":4924,"children":4925},{},[4926],{"type":28,"value":4927},"After 6 months, nobody remembers where anything is.",{"type":22,"tag":31,"props":4929,"children":4930},{},[4931,4936],{"type":22,"tag":50,"props":4932,"children":4933},{},[4934],{"type":28,"value":4935},"Rails Model",{"type":28,"value":4937}," (what some teams copy):",{"type":22,"tag":133,"props":4939,"children":4941},{"code":4940},"src\u002F\n├── views\u002F\n├── components\u002F\n├── composables\u002F\n├── stores\u002F\n├── utils\u002F\n└── services\u002F\n",[4942],{"type":22,"tag":87,"props":4943,"children":4944},{"__ignoreMap":7},[4945],{"type":28,"value":4940},{"type":22,"tag":31,"props":4947,"children":4948},{},[4949],{"type":28,"value":4950},"Better, but still unclear what belongs where.",{"type":22,"tag":31,"props":4952,"children":4953},{},[4954,4959],{"type":22,"tag":50,"props":4955,"children":4956},{},[4957],{"type":28,"value":4958},"Domain-Driven Model",{"type":28,"value":4960}," (what scales):",{"type":22,"tag":133,"props":4962,"children":4964},{"code":4963},"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",[4965],{"type":22,"tag":87,"props":4966,"children":4967},{"__ignoreMap":7},[4968],{"type":28,"value":4963},{"type":22,"tag":31,"props":4970,"children":4971},{},[4972],{"type":28,"value":4973},"We'll focus on the domain-driven model.",{"type":22,"tag":23,"props":4975,"children":4977},{"id":4976},"the-domain-driven-structure",[4978],{"type":28,"value":4979},"The Domain-Driven Structure",{"type":22,"tag":31,"props":4981,"children":4982},{},[4983,4985,4990],{"type":28,"value":4984},"Organize by ",{"type":22,"tag":50,"props":4986,"children":4987},{},[4988],{"type":28,"value":4989},"business domain",{"type":28,"value":4991},", not technical layer:",{"type":22,"tag":133,"props":4993,"children":4995},{"code":4994},"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",[4996],{"type":22,"tag":87,"props":4997,"children":4998},{"__ignoreMap":7},[4999],{"type":28,"value":4994},{"type":22,"tag":31,"props":5001,"children":5002},{},[5003],{"type":28,"value":1665},{"type":22,"tag":42,"props":5005,"children":5006},{},[5007,5017,5041,5051],{"type":22,"tag":46,"props":5008,"children":5009},{},[5010,5015],{"type":22,"tag":50,"props":5011,"children":5012},{},[5013],{"type":28,"value":5014},"Feature cohesion",{"type":28,"value":5016}," — all code for \"users\" is in one place",{"type":22,"tag":46,"props":5018,"children":5019},{},[5020,5025,5027,5033,5035],{"type":22,"tag":50,"props":5021,"children":5022},{},[5023],{"type":28,"value":5024},"Independent shipping",{"type":28,"value":5026}," — one team owns ",{"type":22,"tag":87,"props":5028,"children":5030},{"className":5029},[],[5031],{"type":28,"value":5032},"domains\u002Fusers",{"type":28,"value":5034},", another owns ",{"type":22,"tag":87,"props":5036,"children":5038},{"className":5037},[],[5039],{"type":28,"value":5040},"domains\u002Fbilling",{"type":22,"tag":46,"props":5042,"children":5043},{},[5044,5049],{"type":22,"tag":50,"props":5045,"children":5046},{},[5047],{"type":28,"value":5048},"Clear dependencies",{"type":28,"value":5050}," — easy to see what each domain needs",{"type":22,"tag":46,"props":5052,"children":5053},{},[5054,5059],{"type":22,"tag":50,"props":5055,"children":5056},{},[5057],{"type":28,"value":5058},"Scaling",{"type":28,"value":5060}," — add new domains without touching existing ones",{"type":22,"tag":23,"props":5062,"children":5064},{"id":5063},"the-barrel-export-pattern",[5065],{"type":28,"value":5066},"The Barrel Export Pattern",{"type":22,"tag":31,"props":5068,"children":5069},{},[5070],{"type":28,"value":5071},"Each domain exports a public API:",{"type":22,"tag":133,"props":5073,"children":5076},{"code":5074,"language":138,"meta":7,"className":5075},"\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",[136],[5077],{"type":22,"tag":87,"props":5078,"children":5079},{"__ignoreMap":7},[5080],{"type":28,"value":5074},{"type":22,"tag":31,"props":5082,"children":5083},{},[5084],{"type":28,"value":5085},"Usage:",{"type":22,"tag":133,"props":5087,"children":5090},{"code":5088,"language":138,"meta":7,"className":5089},"\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",[136],[5091],{"type":22,"tag":87,"props":5092,"children":5093},{"__ignoreMap":7},[5094],{"type":28,"value":5088},{"type":22,"tag":31,"props":5096,"children":5097},{},[5098],{"type":28,"value":5099},"Enforce this with eslint:",{"type":22,"tag":133,"props":5101,"children":5104},{"code":5102,"language":312,"meta":7,"className":5103},"\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",[310],[5105],{"type":22,"tag":87,"props":5106,"children":5107},{"__ignoreMap":7},[5108],{"type":28,"value":5102},{"type":22,"tag":23,"props":5110,"children":5112},{"id":5111},"composables-the-shared-logic",[5113],{"type":28,"value":5114},"Composables: The Shared Logic",{"type":22,"tag":31,"props":5116,"children":5117},{},[5118],{"type":28,"value":5119},"Composables are where reusable logic lives:",{"type":22,"tag":133,"props":5121,"children":5124},{"code":5122,"language":138,"meta":7,"className":5123},"\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",[136],[5125],{"type":22,"tag":87,"props":5126,"children":5127},{"__ignoreMap":7},[5128],{"type":28,"value":5122},{"type":22,"tag":31,"props":5130,"children":5131},{},[5132],{"type":28,"value":5133},"Use it:",{"type":22,"tag":133,"props":5135,"children":5138},{"code":5136,"language":1167,"meta":7,"className":5137},"\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",[1165],[5139],{"type":22,"tag":87,"props":5140,"children":5141},{"__ignoreMap":7},[5142],{"type":28,"value":5136},{"type":22,"tag":23,"props":5144,"children":5146},{"id":5145},"dependency-injection",[5147],{"type":28,"value":5148},"Dependency Injection",{"type":22,"tag":31,"props":5150,"children":5151},{},[5152],{"type":28,"value":5153},"For large apps, inject dependencies instead of hardcoding:",{"type":22,"tag":133,"props":5155,"children":5158},{"code":5156,"language":138,"meta":7,"className":5157},"\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",[136],[5159],{"type":22,"tag":87,"props":5160,"children":5161},{"__ignoreMap":7},[5162],{"type":28,"value":5156},{"type":22,"tag":31,"props":5164,"children":5165},{},[5166],{"type":28,"value":5133},{"type":22,"tag":133,"props":5168,"children":5171},{"code":5169,"language":138,"meta":7,"className":5170},"\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",[136],[5172],{"type":22,"tag":87,"props":5173,"children":5174},{"__ignoreMap":7},[5175],{"type":28,"value":5169},{"type":22,"tag":23,"props":5177,"children":5179},{"id":5178},"types-organization",[5180],{"type":28,"value":5181},"Types Organization",{"type":22,"tag":31,"props":5183,"children":5184},{},[5185],{"type":28,"value":5186},"Centralize types:",{"type":22,"tag":133,"props":5188,"children":5191},{"code":5189,"language":138,"meta":7,"className":5190},"\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",[136],[5192],{"type":22,"tag":87,"props":5193,"children":5194},{"__ignoreMap":7},[5195],{"type":28,"value":5189},{"type":22,"tag":31,"props":5197,"children":5198},{},[5199],{"type":28,"value":5200},"Never use inline interfaces.",{"type":22,"tag":23,"props":5202,"children":5204},{"id":5203},"naming-conventions",[5205],{"type":28,"value":5206},"Naming Conventions",{"type":22,"tag":133,"props":5208,"children":5211},{"code":5209,"language":138,"meta":7,"className":5210},"\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",[136],[5212],{"type":22,"tag":87,"props":5213,"children":5214},{"__ignoreMap":7},[5215],{"type":28,"value":5209},{"type":22,"tag":23,"props":5217,"children":5219},{"id":5218},"growing-the-structure",[5220],{"type":28,"value":5221},"Growing the Structure",{"type":22,"tag":31,"props":5223,"children":5224},{},[5225],{"type":28,"value":5226},"As you add more code, follow these patterns:",{"type":22,"tag":133,"props":5228,"children":5230},{"code":5229},"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",[5231],{"type":22,"tag":87,"props":5232,"children":5233},{"__ignoreMap":7},[5234],{"type":28,"value":5229},{"type":22,"tag":23,"props":5236,"children":5238},{"id":5237},"scaling-to-100k-lines",[5239],{"type":28,"value":5240},"Scaling to 100K+ Lines",{"type":22,"tag":31,"props":5242,"children":5243},{},[5244],{"type":28,"value":5245},"At scale, add:",{"type":22,"tag":133,"props":5247,"children":5249},{"code":5248},"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",[5250],{"type":22,"tag":87,"props":5251,"children":5252},{"__ignoreMap":7},[5253],{"type":28,"value":5248},{"type":22,"tag":23,"props":5255,"children":5257},{"id":5256},"key-rules",[5258],{"type":28,"value":5259},"Key Rules",{"type":22,"tag":179,"props":5261,"children":5262},{},[5263,5273,5283,5293,5303,5319],{"type":22,"tag":46,"props":5264,"children":5265},{},[5266,5271],{"type":22,"tag":50,"props":5267,"children":5268},{},[5269],{"type":28,"value":5270},"Keep domains independent",{"type":28,"value":5272}," — Users domain can't import from Billing",{"type":22,"tag":46,"props":5274,"children":5275},{},[5276,5281],{"type":22,"tag":50,"props":5277,"children":5278},{},[5279],{"type":28,"value":5280},"Shared is truly shared",{"type":28,"value":5282}," — only reusable across domains",{"type":22,"tag":46,"props":5284,"children":5285},{},[5286,5291],{"type":22,"tag":50,"props":5287,"children":5288},{},[5289],{"type":28,"value":5290},"Use barrel exports",{"type":28,"value":5292}," — clean public APIs",{"type":22,"tag":46,"props":5294,"children":5295},{},[5296,5301],{"type":22,"tag":50,"props":5297,"children":5298},{},[5299],{"type":28,"value":5300},"Type everything",{"type":28,"value":5302}," — centralize types by domain",{"type":22,"tag":46,"props":5304,"children":5305},{},[5306,5311,5313],{"type":22,"tag":50,"props":5307,"children":5308},{},[5309],{"type":28,"value":5310},"One entry point",{"type":28,"value":5312}," — each domain exports from ",{"type":22,"tag":87,"props":5314,"children":5316},{"className":5315},[],[5317],{"type":28,"value":5318},"index.ts",{"type":22,"tag":46,"props":5320,"children":5321},{},[5322,5327],{"type":22,"tag":50,"props":5323,"children":5324},{},[5325],{"type":28,"value":5326},"Enforce with linting",{"type":28,"value":5328}," — catch violations early",{"type":22,"tag":31,"props":5330,"children":5331},{},[5332],{"type":28,"value":5333},"This structure scales from 10 engineers to 100. Your codebase will thank you.",{"title":7,"searchDepth":551,"depth":551,"links":5335},[5336,5337,5338,5339,5340,5341,5342,5343,5344,5345],{"id":4896,"depth":551,"text":4899},{"id":4976,"depth":551,"text":4979},{"id":5063,"depth":551,"text":5066},{"id":5111,"depth":551,"text":5114},{"id":5145,"depth":551,"text":5148},{"id":5178,"depth":551,"text":5181},{"id":5203,"depth":551,"text":5206},{"id":5218,"depth":551,"text":5221},{"id":5237,"depth":551,"text":5240},{"id":5256,"depth":551,"text":5259},"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":5350,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":5351,"description":5352,"date":5353,"updated":11,"readingTime":5354,"tags":5355,"featured":6,"body":5356,"_type":564,"_id":6080,"_source":566,"_file":6081,"_stem":6082,"_extension":569},"\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,[16],{"type":19,"children":5357,"toc":6058},[5358,5364,5376,5388,5419,5424,5430,5435,5444,5449,5467,5472,5528,5534,5540,5549,5555,5564,5570,5579,5585,5594,5599,5617,5627,5633,5638,5647,5653,5662,5668,5677,5690,5695,5713,5719,5724,5733,5742,5747,5756,5761,5767,5773,5782,5788,5797,5803,5813,5908,5920,5938,5944,5997,6001,6053],{"type":22,"tag":23,"props":5359,"children":5361},{"id":5360},"the-bundle-problem",[5362],{"type":28,"value":5363},"The Bundle Problem",{"type":22,"tag":31,"props":5365,"children":5366},{},[5367,5369,5374],{"type":28,"value":5368},"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":22,"tag":50,"props":5370,"children":5371},{},[5372],{"type":28,"value":5373},"35%",{"type":28,"value":5375}," using the approach in this guide — the strategy below is the repeatable part.",{"type":22,"tag":31,"props":5377,"children":5378},{},[5379,5381,5386],{"type":28,"value":5380},"To make the mechanics concrete, this guide walks through a representative large Vue app. ",{"type":22,"tag":50,"props":5382,"children":5383},{},[5384],{"type":28,"value":5385},"All specific sizes below are an illustrative example",{"type":28,"value":5387},", not measurements from one project:",{"type":22,"tag":42,"props":5389,"children":5390},{},[5391,5400,5409],{"type":22,"tag":46,"props":5392,"children":5393},{},[5394,5398],{"type":22,"tag":50,"props":5395,"children":5396},{},[5397],{"type":28,"value":1080},{"type":28,"value":5399},": 850 KB (gzipped: 280 KB)",{"type":22,"tag":46,"props":5401,"children":5402},{},[5403,5407],{"type":22,"tag":50,"props":5404,"children":5405},{},[5406],{"type":28,"value":1090},{"type":28,"value":5408},": 450 KB (gzipped: 150 KB)",{"type":22,"tag":46,"props":5410,"children":5411},{},[5412,5417],{"type":22,"tag":50,"props":5413,"children":5414},{},[5415],{"type":28,"value":5416},"Total",{"type":28,"value":5418},": 1.2 MB gzipped",{"type":22,"tag":31,"props":5420,"children":5421},{},[5422],{"type":28,"value":5423},"At 3G speeds (1 Mbps), that's 10+ seconds just for JavaScript.",{"type":22,"tag":23,"props":5425,"children":5427},{"id":5426},"step-1-analyze-the-damage",[5428],{"type":28,"value":5429},"Step 1: Analyze the Damage",{"type":22,"tag":31,"props":5431,"children":5432},{},[5433],{"type":28,"value":5434},"First, visualize where bytes are going:",{"type":22,"tag":133,"props":5436,"children":5439},{"className":5437,"code":5438,"language":1059,"meta":7},[1057],"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",[5440],{"type":22,"tag":87,"props":5441,"children":5442},{"__ignoreMap":7},[5443],{"type":28,"value":5438},{"type":22,"tag":31,"props":5445,"children":5446},{},[5447],{"type":28,"value":5448},"You'll see:",{"type":22,"tag":42,"props":5450,"children":5451},{},[5452,5457,5462],{"type":22,"tag":46,"props":5453,"children":5454},{},[5455],{"type":28,"value":5456},"What packages are large",{"type":22,"tag":46,"props":5458,"children":5459},{},[5460],{"type":28,"value":5461},"What's imported but unused",{"type":22,"tag":46,"props":5463,"children":5464},{},[5465],{"type":28,"value":5466},"Duplicate dependencies at different versions",{"type":22,"tag":31,"props":5468,"children":5469},{},[5470],{"type":28,"value":5471},"A typical analysis reveals (illustrative example):",{"type":22,"tag":42,"props":5473,"children":5474},{},[5475,5486,5504,5515],{"type":22,"tag":46,"props":5476,"children":5477},{},[5478,5484],{"type":22,"tag":87,"props":5479,"children":5481},{"className":5480},[],[5482],{"type":28,"value":5483},"lodash-es",{"type":28,"value":5485},": 71 KB (for 3 functions actually used)",{"type":22,"tag":46,"props":5487,"children":5488},{},[5489,5495,5497,5503],{"type":22,"tag":87,"props":5490,"children":5492},{"className":5491},[],[5493],{"type":28,"value":5494},"moment",{"type":28,"value":5496},": 65 KB (replaced with ",{"type":22,"tag":87,"props":5498,"children":5500},{"className":5499},[],[5501],{"type":28,"value":5502},"date-fns",{"type":28,"value":4294},{"type":22,"tag":46,"props":5505,"children":5506},{},[5507,5513],{"type":22,"tag":87,"props":5508,"children":5510},{"className":5509},[],[5511],{"type":28,"value":5512},"chart.js",{"type":28,"value":5514},": 120 KB (only used on one page)",{"type":22,"tag":46,"props":5516,"children":5517},{},[5518,5520,5526],{"type":28,"value":5519},"Duplicate ",{"type":22,"tag":87,"props":5521,"children":5523},{"className":5522},[],[5524],{"type":28,"value":5525},"axios",{"type":28,"value":5527},": two versions imported by different packages",{"type":22,"tag":23,"props":5529,"children":5531},{"id":5530},"step-2-tree-shake-aggressively",[5532],{"type":28,"value":5533},"Step 2: Tree-Shake Aggressively",{"type":22,"tag":107,"props":5535,"children":5537},{"id":5536},"remove-unused-packages",[5538],{"type":28,"value":5539},"Remove Unused Packages",{"type":22,"tag":133,"props":5541,"children":5544},{"className":5542,"code":5543,"language":1059,"meta":7},[1057],"# Find unused dependencies\nnpm install -g depcheck\ndepcheck\n\n# Removes 50+ KB of dead code\nnpm uninstall unused-package\n",[5545],{"type":22,"tag":87,"props":5546,"children":5547},{"__ignoreMap":7},[5548],{"type":28,"value":5543},{"type":22,"tag":107,"props":5550,"children":5552},{"id":5551},"replace-heavy-packages",[5553],{"type":28,"value":5554},"Replace Heavy Packages",{"type":22,"tag":133,"props":5556,"children":5559},{"className":5557,"code":5558,"language":138,"meta":7},[136],"\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",[5560],{"type":22,"tag":87,"props":5561,"children":5562},{"__ignoreMap":7},[5563],{"type":28,"value":5558},{"type":22,"tag":107,"props":5565,"children":5567},{"id":5566},"dependency-consolidation",[5568],{"type":28,"value":5569},"Dependency Consolidation",{"type":22,"tag":133,"props":5571,"children":5574},{"className":5572,"code":5573,"language":138,"meta":7},[136],"\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",[5575],{"type":22,"tag":87,"props":5576,"children":5577},{"__ignoreMap":7},[5578],{"type":28,"value":5573},{"type":22,"tag":107,"props":5580,"children":5582},{"id":5581},"build-config-for-tree-shaking",[5583],{"type":28,"value":5584},"Build Config for Tree-Shaking",{"type":22,"tag":133,"props":5586,"children":5589},{"className":5587,"code":5588,"language":138,"meta":7},[136],"\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",[5590],{"type":22,"tag":87,"props":5591,"children":5592},{"__ignoreMap":7},[5593],{"type":28,"value":5588},{"type":22,"tag":31,"props":5595,"children":5596},{},[5597],{"type":28,"value":5598},"Results for the illustrative example app:",{"type":22,"tag":42,"props":5600,"children":5601},{},[5602,5607,5612],{"type":22,"tag":46,"props":5603,"children":5604},{},[5605],{"type":28,"value":5606},"Removed unused: -150 KB",{"type":22,"tag":46,"props":5608,"children":5609},{},[5610],{"type":28,"value":5611},"Replaced heavy packages: -120 KB",{"type":22,"tag":46,"props":5613,"children":5614},{},[5615],{"type":28,"value":5616},"Tree-shaking optimized: -40 KB",{"type":22,"tag":31,"props":5618,"children":5619},{},[5620,5625],{"type":22,"tag":50,"props":5621,"children":5622},{},[5623],{"type":28,"value":5624},"Total: -310 KB (27% reduction)",{"type":28,"value":5626}," — again, illustrative; the ratio between the three buckets is what generalizes.",{"type":22,"tag":23,"props":5628,"children":5630},{"id":5629},"step-3-smart-code-splitting",[5631],{"type":28,"value":5632},"Step 3: Smart Code Splitting",{"type":22,"tag":31,"props":5634,"children":5635},{},[5636],{"type":28,"value":5637},"Load code only when needed:",{"type":22,"tag":133,"props":5639,"children":5642},{"className":5640,"code":5641,"language":138,"meta":7},[136],"\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",[5643],{"type":22,"tag":87,"props":5644,"children":5645},{"__ignoreMap":7},[5646],{"type":28,"value":5641},{"type":22,"tag":107,"props":5648,"children":5650},{"id":5649},"split-heavy-features",[5651],{"type":28,"value":5652},"Split Heavy Features",{"type":22,"tag":133,"props":5654,"children":5657},{"className":5655,"code":5656,"language":138,"meta":7},[136],"\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",[5658],{"type":22,"tag":87,"props":5659,"children":5660},{"__ignoreMap":7},[5661],{"type":28,"value":5656},{"type":22,"tag":107,"props":5663,"children":5665},{"id":5664},"webpack-magic-comment",[5666],{"type":28,"value":5667},"Webpack Magic Comment",{"type":22,"tag":133,"props":5669,"children":5672},{"className":5670,"code":5671,"language":138,"meta":7},[136],"\u002F\u002F Name the chunk for better debugging\nconst Dashboard = () =>\n  import(\n    \u002F* webpackChunkName: \"dashboard\" *\u002F\n    '@\u002Fpages\u002FDashboard.vue'\n  )\n",[5673],{"type":22,"tag":87,"props":5674,"children":5675},{"__ignoreMap":7},[5676],{"type":28,"value":5671},{"type":22,"tag":31,"props":5678,"children":5679},{},[5680,5682,5688],{"type":28,"value":5681},"Generates: ",{"type":22,"tag":87,"props":5683,"children":5685},{"className":5684},[],[5686],{"type":28,"value":5687},"dist\u002Fdashboard.abc123.js",{"type":28,"value":5689}," (clear what it contains)",{"type":22,"tag":31,"props":5691,"children":5692},{},[5693],{"type":28,"value":5694},"Results of code splitting (illustrative example):",{"type":22,"tag":42,"props":5696,"children":5697},{},[5698,5703,5708],{"type":22,"tag":46,"props":5699,"children":5700},{},[5701],{"type":28,"value":5702},"The main bundle shrinks to what the first route actually needs",{"type":22,"tag":46,"props":5704,"children":5705},{},[5706],{"type":28,"value":5707},"Lazy routes load in small on-demand chunks",{"type":22,"tag":46,"props":5709,"children":5710},{},[5711],{"type":28,"value":5712},"First page load stops paying for pages the user never visits",{"type":22,"tag":23,"props":5714,"children":5716},{"id":5715},"step-4-monitor-continuously",[5717],{"type":28,"value":5718},"Step 4: Monitor Continuously",{"type":22,"tag":31,"props":5720,"children":5721},{},[5722],{"type":28,"value":5723},"Prevent regressions:",{"type":22,"tag":133,"props":5725,"children":5728},{"className":5726,"code":5727,"language":1059,"meta":7},[1057],"npm install -D bundlesize\n",[5729],{"type":22,"tag":87,"props":5730,"children":5731},{"__ignoreMap":7},[5732],{"type":28,"value":5727},{"type":22,"tag":133,"props":5734,"children":5737},{"className":5735,"code":5736,"language":1932,"meta":7},[1934],"\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",[5738],{"type":22,"tag":87,"props":5739,"children":5740},{"__ignoreMap":7},[5741],{"type":28,"value":5736},{"type":22,"tag":31,"props":5743,"children":5744},{},[5745],{"type":28,"value":5746},"GitHub Action:",{"type":22,"tag":133,"props":5748,"children":5751},{"className":5749,"code":5750,"language":3721,"meta":7},[3723],"# .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",[5752],{"type":22,"tag":87,"props":5753,"children":5754},{"__ignoreMap":7},[5755],{"type":28,"value":5750},{"type":22,"tag":31,"props":5757,"children":5758},{},[5759],{"type":28,"value":5760},"Fails PR if bundles exceed limits.",{"type":22,"tag":23,"props":5762,"children":5764},{"id":5763},"step-5-optimize-imports",[5765],{"type":28,"value":5766},"Step 5: Optimize Imports",{"type":22,"tag":107,"props":5768,"children":5770},{"id":5769},"named-vs-default-exports",[5771],{"type":28,"value":5772},"Named vs Default Exports",{"type":22,"tag":133,"props":5774,"children":5777},{"className":5775,"code":5776,"language":138,"meta":7},[136],"\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",[5778],{"type":22,"tag":87,"props":5779,"children":5780},{"__ignoreMap":7},[5781],{"type":28,"value":5776},{"type":22,"tag":107,"props":5783,"children":5785},{"id":5784},"avoid-wildcard-imports",[5786],{"type":28,"value":5787},"Avoid Wildcard Imports",{"type":22,"tag":133,"props":5789,"children":5792},{"className":5790,"code":5791,"language":138,"meta":7},[136],"\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",[5793],{"type":22,"tag":87,"props":5794,"children":5795},{"__ignoreMap":7},[5796],{"type":28,"value":5791},{"type":22,"tag":23,"props":5798,"children":5800},{"id":5799},"results-ongoing-monitoring",[5801],{"type":28,"value":5802},"Results & Ongoing Monitoring",{"type":22,"tag":31,"props":5804,"children":5805},{},[5806,5811],{"type":22,"tag":50,"props":5807,"children":5808},{},[5809],{"type":28,"value":5810},"Before & after for the illustrative example app",{"type":28,"value":5812}," (your ratios will vary; the direction won't):",{"type":22,"tag":1376,"props":5814,"children":5815},{},[5816,5838],{"type":22,"tag":1380,"props":5817,"children":5818},{},[5819],{"type":22,"tag":1384,"props":5820,"children":5821},{},[5822,5826,5830,5834],{"type":22,"tag":1388,"props":5823,"children":5824},{},[5825],{"type":28,"value":1392},{"type":22,"tag":1388,"props":5827,"children":5828},{},[5829],{"type":28,"value":1397},{"type":22,"tag":1388,"props":5831,"children":5832},{},[5833],{"type":28,"value":1402},{"type":22,"tag":1388,"props":5835,"children":5836},{},[5837],{"type":28,"value":1407},{"type":22,"tag":1409,"props":5839,"children":5840},{},[5841,5864,5886],{"type":22,"tag":1384,"props":5842,"children":5843},{},[5844,5849,5854,5859],{"type":22,"tag":1416,"props":5845,"children":5846},{},[5847],{"type":28,"value":5848},"Main Bundle",{"type":22,"tag":1416,"props":5850,"children":5851},{},[5852],{"type":28,"value":5853},"850 KB",{"type":22,"tag":1416,"props":5855,"children":5856},{},[5857],{"type":28,"value":5858},"~300 KB",{"type":22,"tag":1416,"props":5860,"children":5861},{},[5862],{"type":28,"value":5863},"large",{"type":22,"tag":1384,"props":5865,"children":5866},{},[5867,5872,5877,5882],{"type":22,"tag":1416,"props":5868,"children":5869},{},[5870],{"type":28,"value":5871},"Vendor Bundle",{"type":22,"tag":1416,"props":5873,"children":5874},{},[5875],{"type":28,"value":5876},"450 KB",{"type":22,"tag":1416,"props":5878,"children":5879},{},[5880],{"type":28,"value":5881},"~180 KB",{"type":22,"tag":1416,"props":5883,"children":5884},{},[5885],{"type":28,"value":5863},{"type":22,"tag":1384,"props":5887,"children":5888},{},[5889,5894,5899,5904],{"type":22,"tag":1416,"props":5890,"children":5891},{},[5892],{"type":28,"value":5893},"First Paint (3G)",{"type":22,"tag":1416,"props":5895,"children":5896},{},[5897],{"type":28,"value":5898},"~8s",{"type":22,"tag":1416,"props":5900,"children":5901},{},[5902],{"type":28,"value":5903},"~2s",{"type":22,"tag":1416,"props":5905,"children":5906},{},[5907],{"type":28,"value":5863},{"type":22,"tag":31,"props":5909,"children":5910},{},[5911,5913,5918],{"type":28,"value":5912},"In my own production use of this strategy, the verified outcome was a ",{"type":22,"tag":50,"props":5914,"children":5915},{},[5916],{"type":28,"value":5917},"35% bundle reduction",{"type":28,"value":5919},". Beyond raw size:",{"type":22,"tag":42,"props":5921,"children":5922},{},[5923,5928,5933],{"type":22,"tag":46,"props":5924,"children":5925},{},[5926],{"type":28,"value":5927},"Faster for all users, especially mobile",{"type":22,"tag":46,"props":5929,"children":5930},{},[5931],{"type":28,"value":5932},"Better SEO (Core Web Vitals improvement)",{"type":22,"tag":46,"props":5934,"children":5935},{},[5936],{"type":28,"value":5937},"Less battery drain on mobile",{"type":22,"tag":23,"props":5939,"children":5941},{"id":5940},"ongoing-strategy",[5942],{"type":28,"value":5943},"Ongoing Strategy",{"type":22,"tag":179,"props":5945,"children":5946},{},[5947,5957,5967,5977,5987],{"type":22,"tag":46,"props":5948,"children":5949},{},[5950,5955],{"type":22,"tag":50,"props":5951,"children":5952},{},[5953],{"type":28,"value":5954},"Weekly monitoring",{"type":28,"value":5956}," — bundlesize checks every PR",{"type":22,"tag":46,"props":5958,"children":5959},{},[5960,5965],{"type":22,"tag":50,"props":5961,"children":5962},{},[5963],{"type":28,"value":5964},"Quarterly audits",{"type":28,"value":5966}," — analyze with visualizer",{"type":22,"tag":46,"props":5968,"children":5969},{},[5970,5975],{"type":22,"tag":50,"props":5971,"children":5972},{},[5973],{"type":28,"value":5974},"Department rotation",{"type":28,"value":5976}," — one engineer owns bundle each month",{"type":22,"tag":46,"props":5978,"children":5979},{},[5980,5985],{"type":22,"tag":50,"props":5981,"children":5982},{},[5983],{"type":28,"value":5984},"Team education",{"type":28,"value":5986}," — awareness of bundle impact",{"type":22,"tag":46,"props":5988,"children":5989},{},[5990,5995],{"type":22,"tag":50,"props":5991,"children":5992},{},[5993],{"type":28,"value":5994},"Dependency reviews",{"type":28,"value":5996}," — approve new packages by size",{"type":22,"tag":23,"props":5998,"children":5999},{"id":3946},[6000],{"type":28,"value":3949},{"type":22,"tag":179,"props":6002,"children":6003},{},[6004,6014,6024,6034,6044],{"type":22,"tag":46,"props":6005,"children":6006},{},[6007,6012],{"type":22,"tag":50,"props":6008,"children":6009},{},[6010],{"type":28,"value":6011},"Lazy load routes by default",{"type":28,"value":6013}," — all routes should be dynamic",{"type":22,"tag":46,"props":6015,"children":6016},{},[6017,6022],{"type":22,"tag":50,"props":6018,"children":6019},{},[6020],{"type":28,"value":6021},"Split by feature",{"type":28,"value":6023}," — heavy features in separate chunks",{"type":22,"tag":46,"props":6025,"children":6026},{},[6027,6032],{"type":22,"tag":50,"props":6028,"children":6029},{},[6030],{"type":28,"value":6031},"Use es2015 module syntax",{"type":28,"value":6033}," — enables tree-shaking",{"type":22,"tag":46,"props":6035,"children":6036},{},[6037,6042],{"type":22,"tag":50,"props":6038,"children":6039},{},[6040],{"type":28,"value":6041},"Compress with Brotli",{"type":28,"value":6043}," — 15-20% smaller than gzip",{"type":22,"tag":46,"props":6045,"children":6046},{},[6047,6051],{"type":22,"tag":50,"props":6048,"children":6049},{},[6050],{"type":28,"value":1549},{"type":28,"value":6052}," — real user metrics matter most",{"type":22,"tag":31,"props":6054,"children":6055},{},[6056],{"type":28,"value":6057},"Bundle size affects every user, every time. It's worth optimizing.",{"title":7,"searchDepth":551,"depth":551,"links":6059},[6060,6061,6062,6068,6072,6073,6077,6078,6079],{"id":5360,"depth":551,"text":5363},{"id":5426,"depth":551,"text":5429},{"id":5530,"depth":551,"text":5533,"children":6063},[6064,6065,6066,6067],{"id":5536,"depth":557,"text":5539},{"id":5551,"depth":557,"text":5554},{"id":5566,"depth":557,"text":5569},{"id":5581,"depth":557,"text":5584},{"id":5629,"depth":551,"text":5632,"children":6069},[6070,6071],{"id":5649,"depth":557,"text":5652},{"id":5664,"depth":557,"text":5667},{"id":5715,"depth":551,"text":5718},{"id":5763,"depth":551,"text":5766,"children":6074},[6075,6076],{"id":5769,"depth":557,"text":5772},{"id":5784,"depth":557,"text":5787},{"id":5799,"depth":551,"text":5802},{"id":5940,"depth":551,"text":5943},{"id":3946,"depth":551,"text":3949},"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":6084,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":6085,"description":6086,"date":6087,"readingTime":5354,"tags":6088,"featured":6,"body":6091,"_type":564,"_id":6708,"_source":566,"_file":6709,"_stem":6710,"_extension":569},"\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",[6089,6090],"Remote","Culture",{"type":19,"children":6092,"toc":6683},[6093,6099,6104,6109,6131,6141,6147,6153,6158,6166,6171,6179,6184,6237,6243,6248,6256,6261,6267,6272,6277,6287,6292,6298,6303,6309,6318,6323,6329,6338,6344,6353,6359,6368,6374,6392,6398,6403,6411,6416,6421,6426,6431,6439,6444,6450,6455,6461,6466,6474,6480,6488,6494,6503,6509,6517,6523,6528,6581,6587,6592,6634,6640,6650,6655,6678],{"type":22,"tag":23,"props":6094,"children":6096},{"id":6095},"the-remote-frontend-engineer",[6097],{"type":28,"value":6098},"The Remote Frontend Engineer",{"type":22,"tag":31,"props":6100,"children":6101},{},[6102],{"type":28,"value":6103},"Remote work is now standard. Being a remote engineer requires a different mindset than co-located work.",{"type":22,"tag":31,"props":6105,"children":6106},{},[6107],{"type":28,"value":6108},"The difference:",{"type":22,"tag":42,"props":6110,"children":6111},{},[6112,6122],{"type":22,"tag":46,"props":6113,"children":6114},{},[6115,6120],{"type":22,"tag":50,"props":6116,"children":6117},{},[6118],{"type":28,"value":6119},"Co-located",{"type":28,"value":6121},": \"I'll ask Bob on Slack\"",{"type":22,"tag":46,"props":6123,"children":6124},{},[6125,6129],{"type":22,"tag":50,"props":6126,"children":6127},{},[6128],{"type":28,"value":6089},{"type":28,"value":6130},": \"Bob is asleep; I'll find the answer in docs\"",{"type":22,"tag":31,"props":6132,"children":6133},{},[6134,6136],{"type":28,"value":6135},"The principle: ",{"type":22,"tag":50,"props":6137,"children":6138},{},[6139],{"type":28,"value":6140},"Assume your teammates are asleep, in a meeting, or handling an emergency.",{"type":22,"tag":23,"props":6142,"children":6144},{"id":6143},"async-first-communication",[6145],{"type":28,"value":6146},"Async-First Communication",{"type":22,"tag":107,"props":6148,"children":6150},{"id":6149},"write-instead-of-chat",[6151],{"type":28,"value":6152},"Write Instead of Chat",{"type":22,"tag":31,"props":6154,"children":6155},{},[6156],{"type":28,"value":6157},"Bad:",{"type":22,"tag":133,"props":6159,"children":6161},{"code":6160},"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",[6162],{"type":22,"tag":87,"props":6163,"children":6164},{"__ignoreMap":7},[6165],{"type":28,"value":6160},{"type":22,"tag":31,"props":6167,"children":6168},{},[6169],{"type":28,"value":6170},"Good:",{"type":22,"tag":133,"props":6172,"children":6174},{"code":6173},"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",[6175],{"type":22,"tag":87,"props":6176,"children":6177},{"__ignoreMap":7},[6178],{"type":28,"value":6173},{"type":22,"tag":31,"props":6180,"children":6181},{},[6182],{"type":28,"value":6183},"Include:",{"type":22,"tag":42,"props":6185,"children":6186},{},[6187,6197,6207,6217,6227],{"type":22,"tag":46,"props":6188,"children":6189},{},[6190,6195],{"type":22,"tag":50,"props":6191,"children":6192},{},[6193],{"type":28,"value":6194},"Context",{"type":28,"value":6196}," — what you're trying to do",{"type":22,"tag":46,"props":6198,"children":6199},{},[6200,6205],{"type":22,"tag":50,"props":6201,"children":6202},{},[6203],{"type":28,"value":6204},"What you've tried",{"type":28,"value":6206}," — don't ask before researching",{"type":22,"tag":46,"props":6208,"children":6209},{},[6210,6215],{"type":22,"tag":50,"props":6211,"children":6212},{},[6213],{"type":28,"value":6214},"Specific question",{"type":28,"value":6216}," — not \"does this work?\"",{"type":22,"tag":46,"props":6218,"children":6219},{},[6220,6225],{"type":22,"tag":50,"props":6221,"children":6222},{},[6223],{"type":28,"value":6224},"Deadline",{"type":28,"value":6226}," — when you need the answer",{"type":22,"tag":46,"props":6228,"children":6229},{},[6230,6235],{"type":22,"tag":50,"props":6231,"children":6232},{},[6233],{"type":28,"value":6234},"Links",{"type":28,"value":6236}," — PR\u002Fissue references",{"type":22,"tag":107,"props":6238,"children":6240},{"id":6239},"use-threads",[6241],{"type":28,"value":6242},"Use Threads",{"type":22,"tag":31,"props":6244,"children":6245},{},[6246],{"type":28,"value":6247},"Slack threading keeps conversations organized:",{"type":22,"tag":133,"props":6249,"children":6251},{"code":6250},"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",[6252],{"type":22,"tag":87,"props":6253,"children":6254},{"__ignoreMap":7},[6255],{"type":28,"value":6250},{"type":22,"tag":31,"props":6257,"children":6258},{},[6259],{"type":28,"value":6260},"Main channel stays clean. Interested people can see the discussion.",{"type":22,"tag":107,"props":6262,"children":6264},{"id":6263},"status-updates-not-standups",[6265],{"type":28,"value":6266},"Status Updates (Not Standups)",{"type":22,"tag":31,"props":6268,"children":6269},{},[6270],{"type":28,"value":6271},"Bad: Synchronous standup at 9am (3am for India team)",{"type":22,"tag":31,"props":6273,"children":6274},{},[6275],{"type":28,"value":6276},"Good: Async status in shared doc",{"type":22,"tag":133,"props":6278,"children":6282},{"code":6279,"language":564,"meta":7,"className":6280},"## 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",[6281],"language-markdown",[6283],{"type":22,"tag":87,"props":6284,"children":6285},{"__ignoreMap":7},[6286],{"type":28,"value":6279},{"type":22,"tag":31,"props":6288,"children":6289},{},[6290],{"type":28,"value":6291},"This works for everyone, any timezone.",{"type":22,"tag":23,"props":6293,"children":6295},{"id":6294},"documentation-is-law",[6296],{"type":28,"value":6297},"Documentation Is Law",{"type":22,"tag":31,"props":6299,"children":6300},{},[6301],{"type":28,"value":6302},"Remote engineers live in documentation.",{"type":22,"tag":107,"props":6304,"children":6306},{"id":6305},"architecture-decisions",[6307],{"type":28,"value":6308},"Architecture Decisions",{"type":22,"tag":133,"props":6310,"children":6313},{"code":6311,"language":564,"meta":7,"className":6312},"## 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",[6281],[6314],{"type":22,"tag":87,"props":6315,"children":6316},{"__ignoreMap":7},[6317],{"type":28,"value":6311},{"type":22,"tag":31,"props":6319,"children":6320},{},[6321],{"type":28,"value":6322},"Store in shared repo, searchable, link from PRs.",{"type":22,"tag":107,"props":6324,"children":6326},{"id":6325},"component-documentation",[6327],{"type":28,"value":6328},"Component Documentation",{"type":22,"tag":133,"props":6330,"children":6333},{"code":6331,"language":138,"meta":7,"className":6332},"\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",[136],[6334],{"type":22,"tag":87,"props":6335,"children":6336},{"__ignoreMap":7},[6337],{"type":28,"value":6331},{"type":22,"tag":107,"props":6339,"children":6341},{"id":6340},"api-documentation",[6342],{"type":28,"value":6343},"API Documentation",{"type":22,"tag":133,"props":6345,"children":6348},{"code":6346,"language":564,"meta":7,"className":6347},"## 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",[6281],[6349],{"type":22,"tag":87,"props":6350,"children":6351},{"__ignoreMap":7},[6352],{"type":28,"value":6346},{"type":22,"tag":107,"props":6354,"children":6356},{"id":6355},"response",[6357],{"type":28,"value":6358},"Response",{"type":22,"tag":133,"props":6360,"children":6363},{"code":6361,"language":1932,"meta":7,"className":6362},"{\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",[1934],[6364],{"type":22,"tag":87,"props":6365,"children":6366},{"__ignoreMap":7},[6367],{"type":28,"value":6361},{"type":22,"tag":107,"props":6369,"children":6371},{"id":6370},"errors",[6372],{"type":28,"value":6373},"Errors",{"type":22,"tag":42,"props":6375,"children":6376},{},[6377,6382,6387],{"type":22,"tag":46,"props":6378,"children":6379},{},[6380],{"type":28,"value":6381},"400: Invalid email format",{"type":22,"tag":46,"props":6383,"children":6384},{},[6385],{"type":28,"value":6386},"409: Email already exists",{"type":22,"tag":46,"props":6388,"children":6389},{},[6390],{"type":28,"value":6391},"422: Missing required fields",{"type":22,"tag":107,"props":6393,"children":6395},{"id":6394},"rate-limit",[6396],{"type":28,"value":6397},"Rate Limit",{"type":22,"tag":31,"props":6399,"children":6400},{},[6401],{"type":28,"value":6402},"100 requests per minute per API key",{"type":22,"tag":133,"props":6404,"children":6406},{"code":6405},"\n## Code Review Culture\n\nRemote code reviews must be thorough and asynchronous.\n\n### Good Code Review Comment\n\n",[6407],{"type":22,"tag":87,"props":6408,"children":6409},{"__ignoreMap":7},[6410],{"type":28,"value":6405},{"type":22,"tag":31,"props":6412,"children":6413},{},[6414],{"type":28,"value":6415},"\u002F\u002F ❌ Vague\n\"This looks off\"",{"type":22,"tag":31,"props":6417,"children":6418},{},[6419],{"type":28,"value":6420},"\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":22,"tag":31,"props":6422,"children":6423},{},[6424],{"type":28,"value":6425},"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":22,"tag":31,"props":6427,"children":6428},{},[6429],{"type":28,"value":6430},"See similar pattern in useUser.ts line 32\"",{"type":22,"tag":133,"props":6432,"children":6434},{"code":6433},"\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",[6435],{"type":22,"tag":87,"props":6436,"children":6437},{"__ignoreMap":7},[6438],{"type":28,"value":6433},{"type":22,"tag":31,"props":6440,"children":6441},{},[6442],{"type":28,"value":6443},"Use it for every PR.",{"type":22,"tag":23,"props":6445,"children":6447},{"id":6446},"timezone-management",[6448],{"type":28,"value":6449},"Timezone Management",{"type":22,"tag":31,"props":6451,"children":6452},{},[6453],{"type":28,"value":6454},"Working across timezones requires structure:",{"type":22,"tag":107,"props":6456,"children":6458},{"id":6457},"core-collaboration-hours",[6459],{"type":28,"value":6460},"Core Collaboration Hours",{"type":22,"tag":31,"props":6462,"children":6463},{},[6464],{"type":28,"value":6465},"Define overlapping times when synchronous work happens:",{"type":22,"tag":133,"props":6467,"children":6469},{"code":6468},"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",[6470],{"type":22,"tag":87,"props":6471,"children":6472},{"__ignoreMap":7},[6473],{"type":28,"value":6468},{"type":22,"tag":107,"props":6475,"children":6477},{"id":6476},"meeting-best-practices",[6478],{"type":28,"value":6479},"Meeting Best Practices",{"type":22,"tag":133,"props":6481,"children":6483},{"code":6482},"❌ 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",[6484],{"type":22,"tag":87,"props":6485,"children":6486},{"__ignoreMap":7},[6487],{"type":28,"value":6482},{"type":22,"tag":107,"props":6489,"children":6491},{"id":6490},"communication-norms",[6492],{"type":28,"value":6493},"Communication Norms",{"type":22,"tag":133,"props":6495,"children":6498},{"code":6496,"language":564,"meta":7,"className":6497},"## 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",[6281],[6499],{"type":22,"tag":87,"props":6500,"children":6501},{"__ignoreMap":7},[6502],{"type":28,"value":6496},{"type":22,"tag":23,"props":6504,"children":6506},{"id":6505},"tools-that-work-remote",[6507],{"type":28,"value":6508},"Tools That Work Remote",{"type":22,"tag":133,"props":6510,"children":6512},{"code":6511},"✅ 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",[6513],{"type":22,"tag":87,"props":6514,"children":6515},{"__ignoreMap":7},[6516],{"type":28,"value":6511},{"type":22,"tag":23,"props":6518,"children":6520},{"id":6519},"remote-interview-tips-meta",[6521],{"type":28,"value":6522},"Remote Interview Tips (Meta)",{"type":22,"tag":31,"props":6524,"children":6525},{},[6526],{"type":28,"value":6527},"You're hiring a remote engineer. Watch for:",{"type":22,"tag":179,"props":6529,"children":6530},{},[6531,6541,6551,6561,6571],{"type":22,"tag":46,"props":6532,"children":6533},{},[6534,6539],{"type":22,"tag":50,"props":6535,"children":6536},{},[6537],{"type":28,"value":6538},"Written communication",{"type":28,"value":6540}," — can they explain clearly?",{"type":22,"tag":46,"props":6542,"children":6543},{},[6544,6549],{"type":22,"tag":50,"props":6545,"children":6546},{},[6547],{"type":28,"value":6548},"Self-motivation",{"type":28,"value":6550}," — do they ask good questions during prep?",{"type":22,"tag":46,"props":6552,"children":6553},{},[6554,6559],{"type":22,"tag":50,"props":6555,"children":6556},{},[6557],{"type":28,"value":6558},"Timezone awareness",{"type":28,"value":6560}," — do they mention working across timezones?",{"type":22,"tag":46,"props":6562,"children":6563},{},[6564,6569],{"type":22,"tag":50,"props":6565,"children":6566},{},[6567],{"type":28,"value":6568},"Documentation review",{"type":28,"value":6570}," — did they read our docs before interview?",{"type":22,"tag":46,"props":6572,"children":6573},{},[6574,6579],{"type":22,"tag":50,"props":6575,"children":6576},{},[6577],{"type":28,"value":6578},"Async examples",{"type":28,"value":6580}," — how have they worked async before?",{"type":22,"tag":23,"props":6582,"children":6584},{"id":6583},"results-of-good-remote-practices",[6585],{"type":28,"value":6586},"Results of Good Remote Practices",{"type":22,"tag":31,"props":6588,"children":6589},{},[6590],{"type":28,"value":6591},"After adopting these practices:",{"type":22,"tag":42,"props":6593,"children":6594},{},[6595,6605,6614,6624],{"type":22,"tag":46,"props":6596,"children":6597},{},[6598,6603],{"type":22,"tag":50,"props":6599,"children":6600},{},[6601],{"type":28,"value":6602},"Decision speed",{"type":28,"value":6604},": 2 days → 24 hours (async docs > meetings)",{"type":22,"tag":46,"props":6606,"children":6607},{},[6608,6612],{"type":22,"tag":50,"props":6609,"children":6610},{},[6611],{"type":28,"value":2889},{"type":28,"value":6613},": 4 weeks → 1.5 weeks (docs are source of truth)",{"type":22,"tag":46,"props":6615,"children":6616},{},[6617,6622],{"type":22,"tag":50,"props":6618,"children":6619},{},[6620],{"type":28,"value":6621},"Bug reports",{"type":28,"value":6623},": 4\u002Fweek → 1\u002Fweek (context is clear)",{"type":22,"tag":46,"props":6625,"children":6626},{},[6627,6632],{"type":22,"tag":50,"props":6628,"children":6629},{},[6630],{"type":28,"value":6631},"Developer satisfaction",{"type":28,"value":6633},": 7\u002F10 → 9\u002F10 (no sync meeting fatigue)",{"type":22,"tag":23,"props":6635,"children":6637},{"id":6636},"the-secret",[6638],{"type":28,"value":6639},"The Secret",{"type":22,"tag":31,"props":6641,"children":6642},{},[6643,6645],{"type":28,"value":6644},"Remote work isn't about Zoom calls. It's about ",{"type":22,"tag":50,"props":6646,"children":6647},{},[6648],{"type":28,"value":6649},"clear documentation, trust, and autonomy.",{"type":22,"tag":31,"props":6651,"children":6652},{},[6653],{"type":28,"value":6654},"Engineers who can work async:",{"type":22,"tag":42,"props":6656,"children":6657},{},[6658,6663,6668,6673],{"type":22,"tag":46,"props":6659,"children":6660},{},[6661],{"type":28,"value":6662},"Ship faster",{"type":22,"tag":46,"props":6664,"children":6665},{},[6666],{"type":28,"value":6667},"Need less supervision",{"type":22,"tag":46,"props":6669,"children":6670},{},[6671],{"type":28,"value":6672},"Solve problems independently",{"type":22,"tag":46,"props":6674,"children":6675},{},[6676],{"type":28,"value":6677},"Get better sleep (no midnight calls)",{"type":22,"tag":31,"props":6679,"children":6680},{},[6681],{"type":28,"value":6682},"The best remote engineers aren't chatty. They're clear writers.",{"title":7,"searchDepth":551,"depth":551,"links":6684},[6685,6686,6691,6699,6704,6705,6706,6707],{"id":6095,"depth":551,"text":6098},{"id":6143,"depth":551,"text":6146,"children":6687},[6688,6689,6690],{"id":6149,"depth":557,"text":6152},{"id":6239,"depth":557,"text":6242},{"id":6263,"depth":557,"text":6266},{"id":6294,"depth":551,"text":6297,"children":6692},[6693,6694,6695,6696,6697,6698],{"id":6305,"depth":557,"text":6308},{"id":6325,"depth":557,"text":6328},{"id":6340,"depth":557,"text":6343},{"id":6355,"depth":557,"text":6358},{"id":6370,"depth":557,"text":6373},{"id":6394,"depth":557,"text":6397},{"id":6446,"depth":551,"text":6449,"children":6700},[6701,6702,6703],{"id":6457,"depth":557,"text":6460},{"id":6476,"depth":557,"text":6479},{"id":6490,"depth":557,"text":6493},{"id":6505,"depth":551,"text":6508},{"id":6519,"depth":551,"text":6522},{"id":6583,"depth":551,"text":6586},{"id":6636,"depth":551,"text":6639},"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,"updated":11,"readingTime":12,"tags":6712,"featured":17,"body":6713,"_type":564,"_id":565,"_source":566,"_file":567,"_stem":568,"_extension":569},[14,15,16],{"type":19,"children":6714,"toc":7123},[6715,6719,6723,6727,6768,6772,6776,6780,6794,6802,6806,6821,6825,6829,6856,6860,6868,6872,6912,6916,6925,6929,6937,6941,6945,6953,6957,6961,6969,6973,6977,6981,6989,6993,6997,7001,7005,7012,7033,7037,7080,7084,7119],{"type":22,"tag":23,"props":6716,"children":6717},{"id":25},[6718],{"type":28,"value":29},{"type":22,"tag":31,"props":6720,"children":6721},{},[6722],{"type":28,"value":35},{"type":22,"tag":31,"props":6724,"children":6725},{},[6726],{"type":28,"value":40},{"type":22,"tag":42,"props":6728,"children":6729},{},[6730,6738,6746,6754],{"type":22,"tag":46,"props":6731,"children":6732},{},[6733,6737],{"type":22,"tag":50,"props":6734,"children":6735},{},[6736],{"type":28,"value":54},{"type":28,"value":56},{"type":22,"tag":46,"props":6739,"children":6740},{},[6741,6745],{"type":22,"tag":50,"props":6742,"children":6743},{},[6744],{"type":28,"value":64},{"type":28,"value":66},{"type":22,"tag":46,"props":6747,"children":6748},{},[6749,6753],{"type":22,"tag":50,"props":6750,"children":6751},{},[6752],{"type":28,"value":16},{"type":28,"value":75},{"type":22,"tag":46,"props":6755,"children":6756},{},[6757,6761,6762,6767],{"type":22,"tag":50,"props":6758,"children":6759},{},[6760],{"type":28,"value":83},{"type":28,"value":85},{"type":22,"tag":87,"props":6763,"children":6765},{"className":6764},[],[6766],{"type":28,"value":92},{"type":28,"value":94},{"type":22,"tag":31,"props":6769,"children":6770},{},[6771],{"type":28,"value":99},{"type":22,"tag":23,"props":6773,"children":6774},{"id":102},[6775],{"type":28,"value":105},{"type":22,"tag":107,"props":6777,"children":6778},{"id":109},[6779],{"type":28,"value":112},{"type":22,"tag":31,"props":6781,"children":6782},{},[6783,6784,6788,6789,6793],{"type":28,"value":117},{"type":22,"tag":50,"props":6785,"children":6786},{},[6787],{"type":28,"value":122},{"type":28,"value":124},{"type":22,"tag":50,"props":6790,"children":6791},{},[6792],{"type":28,"value":129},{"type":28,"value":131},{"type":22,"tag":133,"props":6795,"children":6797},{"className":6796,"code":137,"language":138,"meta":7},[136],[6798],{"type":22,"tag":87,"props":6799,"children":6800},{"__ignoreMap":7},[6801],{"type":28,"value":137},{"type":22,"tag":31,"props":6803,"children":6804},{},[6805],{"type":28,"value":148},{"type":22,"tag":42,"props":6807,"children":6808},{},[6809,6813,6817],{"type":22,"tag":46,"props":6810,"children":6811},{},[6812],{"type":28,"value":156},{"type":22,"tag":46,"props":6814,"children":6815},{},[6816],{"type":28,"value":161},{"type":22,"tag":46,"props":6818,"children":6819},{},[6820],{"type":28,"value":166},{"type":22,"tag":107,"props":6822,"children":6823},{"id":169},[6824],{"type":28,"value":172},{"type":22,"tag":31,"props":6826,"children":6827},{},[6828],{"type":28,"value":177},{"type":22,"tag":179,"props":6830,"children":6831},{},[6832,6840,6848],{"type":22,"tag":46,"props":6833,"children":6834},{},[6835,6839],{"type":22,"tag":50,"props":6836,"children":6837},{},[6838],{"type":28,"value":189},{"type":28,"value":191},{"type":22,"tag":46,"props":6841,"children":6842},{},[6843,6847],{"type":22,"tag":50,"props":6844,"children":6845},{},[6846],{"type":28,"value":199},{"type":28,"value":201},{"type":22,"tag":46,"props":6849,"children":6850},{},[6851,6855],{"type":22,"tag":50,"props":6852,"children":6853},{},[6854],{"type":28,"value":209},{"type":28,"value":211},{"type":22,"tag":213,"props":6857,"children":6858},{"id":215},[6859],{"type":28,"value":218},{"type":22,"tag":133,"props":6861,"children":6863},{"className":6862,"code":222,"language":138,"meta":7},[136],[6864],{"type":22,"tag":87,"props":6865,"children":6866},{"__ignoreMap":7},[6867],{"type":28,"value":222},{"type":22,"tag":31,"props":6869,"children":6870},{},[6871],{"type":28,"value":232},{"type":22,"tag":42,"props":6873,"children":6874},{},[6875,6883,6891,6899],{"type":22,"tag":46,"props":6876,"children":6877},{},[6878,6882],{"type":22,"tag":50,"props":6879,"children":6880},{},[6881],{"type":28,"value":243},{"type":28,"value":245},{"type":22,"tag":46,"props":6884,"children":6885},{},[6886,6890],{"type":22,"tag":50,"props":6887,"children":6888},{},[6889],{"type":28,"value":253},{"type":28,"value":255},{"type":22,"tag":46,"props":6892,"children":6893},{},[6894,6898],{"type":22,"tag":50,"props":6895,"children":6896},{},[6897],{"type":28,"value":263},{"type":28,"value":265},{"type":22,"tag":46,"props":6900,"children":6901},{},[6902,6906,6907],{"type":22,"tag":50,"props":6903,"children":6904},{},[6905],{"type":28,"value":273},{"type":28,"value":275},{"type":22,"tag":87,"props":6908,"children":6910},{"className":6909},[],[6911],{"type":28,"value":281},{"type":22,"tag":107,"props":6913,"children":6914},{"id":284},[6915],{"type":28,"value":287},{"type":22,"tag":31,"props":6917,"children":6918},{},[6919,6920,6924],{"type":28,"value":292},{"type":22,"tag":294,"props":6921,"children":6922},{},[6923],{"type":28,"value":298},{"type":28,"value":300},{"type":22,"tag":213,"props":6926,"children":6927},{"id":303},[6928],{"type":28,"value":306},{"type":22,"tag":133,"props":6930,"children":6932},{"className":6931,"code":311,"language":312,"meta":7},[310],[6933],{"type":22,"tag":87,"props":6934,"children":6935},{"__ignoreMap":7},[6936],{"type":28,"value":311},{"type":22,"tag":31,"props":6938,"children":6939},{},[6940],{"type":28,"value":322},{"type":22,"tag":213,"props":6942,"children":6943},{"id":325},[6944],{"type":28,"value":328},{"type":22,"tag":133,"props":6946,"children":6948},{"className":6947,"code":332,"language":312,"meta":7},[310],[6949],{"type":22,"tag":87,"props":6950,"children":6951},{"__ignoreMap":7},[6952],{"type":28,"value":332},{"type":22,"tag":31,"props":6954,"children":6955},{},[6956],{"type":28,"value":342},{"type":22,"tag":213,"props":6958,"children":6959},{"id":345},[6960],{"type":28,"value":348},{"type":22,"tag":133,"props":6962,"children":6964},{"className":6963,"code":352,"language":312,"meta":7},[310],[6965],{"type":22,"tag":87,"props":6966,"children":6967},{"__ignoreMap":7},[6968],{"type":28,"value":352},{"type":22,"tag":31,"props":6970,"children":6971},{},[6972],{"type":28,"value":362},{"type":22,"tag":107,"props":6974,"children":6975},{"id":365},[6976],{"type":28,"value":368},{"type":22,"tag":31,"props":6978,"children":6979},{},[6980],{"type":28,"value":373},{"type":22,"tag":133,"props":6982,"children":6984},{"className":6983,"code":377,"language":138,"meta":7},[136],[6985],{"type":22,"tag":87,"props":6986,"children":6987},{"__ignoreMap":7},[6988],{"type":28,"value":377},{"type":22,"tag":31,"props":6990,"children":6991},{},[6992],{"type":28,"value":387},{"type":22,"tag":31,"props":6994,"children":6995},{},[6996],{"type":28,"value":392},{"type":22,"tag":23,"props":6998,"children":6999},{"id":395},[7000],{"type":28,"value":398},{"type":22,"tag":31,"props":7002,"children":7003},{},[7004],{"type":28,"value":403},{"type":22,"tag":31,"props":7006,"children":7007},{},[7008],{"type":22,"tag":50,"props":7009,"children":7010},{},[7011],{"type":28,"value":411},{"type":22,"tag":42,"props":7013,"children":7014},{},[7015,7019,7029],{"type":22,"tag":46,"props":7016,"children":7017},{},[7018],{"type":28,"value":419},{"type":22,"tag":46,"props":7020,"children":7021},{},[7022,7023,7028],{"type":28,"value":424},{"type":22,"tag":87,"props":7024,"children":7026},{"className":7025},[],[7027],{"type":28,"value":281},{"type":28,"value":431},{"type":22,"tag":46,"props":7030,"children":7031},{},[7032],{"type":28,"value":436},{"type":22,"tag":23,"props":7034,"children":7035},{"id":439},[7036],{"type":28,"value":442},{"type":22,"tag":179,"props":7038,"children":7039},{},[7040,7048,7056,7064,7072],{"type":22,"tag":46,"props":7041,"children":7042},{},[7043,7047],{"type":22,"tag":50,"props":7044,"children":7045},{},[7046],{"type":28,"value":453},{"type":28,"value":455},{"type":22,"tag":46,"props":7049,"children":7050},{},[7051,7055],{"type":22,"tag":50,"props":7052,"children":7053},{},[7054],{"type":28,"value":463},{"type":28,"value":465},{"type":22,"tag":46,"props":7057,"children":7058},{},[7059,7063],{"type":22,"tag":50,"props":7060,"children":7061},{},[7062],{"type":28,"value":473},{"type":28,"value":475},{"type":22,"tag":46,"props":7065,"children":7066},{},[7067,7071],{"type":22,"tag":50,"props":7068,"children":7069},{},[7070],{"type":28,"value":483},{"type":28,"value":485},{"type":22,"tag":46,"props":7073,"children":7074},{},[7075,7079],{"type":22,"tag":50,"props":7076,"children":7077},{},[7078],{"type":28,"value":493},{"type":28,"value":495},{"type":22,"tag":23,"props":7081,"children":7082},{"id":498},[7083],{"type":28,"value":501},{"type":22,"tag":42,"props":7085,"children":7086},{},[7087,7095,7103,7111],{"type":22,"tag":46,"props":7088,"children":7089},{},[7090,7094],{"type":22,"tag":50,"props":7091,"children":7092},{},[7093],{"type":28,"value":512},{"type":28,"value":514},{"type":22,"tag":46,"props":7096,"children":7097},{},[7098,7102],{"type":22,"tag":50,"props":7099,"children":7100},{},[7101],{"type":28,"value":522},{"type":28,"value":524},{"type":22,"tag":46,"props":7104,"children":7105},{},[7106,7110],{"type":22,"tag":50,"props":7107,"children":7108},{},[7109],{"type":28,"value":532},{"type":28,"value":534},{"type":22,"tag":46,"props":7112,"children":7113},{},[7114,7118],{"type":22,"tag":50,"props":7115,"children":7116},{},[7117],{"type":28,"value":542},{"type":28,"value":544},{"type":22,"tag":31,"props":7120,"children":7121},{},[7122],{"type":28,"value":549},{"title":7,"searchDepth":551,"depth":551,"links":7124},[7125,7126,7132,7133,7134],{"id":25,"depth":551,"text":29},{"id":102,"depth":551,"text":105,"children":7127},[7128,7129,7130,7131],{"id":109,"depth":557,"text":112},{"id":169,"depth":557,"text":172},{"id":284,"depth":557,"text":287},{"id":365,"depth":557,"text":368},{"id":395,"depth":551,"text":398},{"id":439,"depth":551,"text":442},{"id":498,"depth":551,"text":501},1783235261579]