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