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