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