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