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