[{"data":1,"prerenderedAt":1057},["ShallowReactive",2],{"content:\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports":3,"surroundings:\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports":1048},{"id":4,"title":5,"body":6,"description":1029,"extension":1030,"meta":1031,"navigation":374,"path":1043,"seo":1044,"stem":1046,"__hash__":1047},"content\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports\u002Findex.md","Writing Tree-Shakeable Library Exports",{"type":7,"value":8,"toc":1013},"minimark",[9,14,38,45,158,163,200,204,210,220,226,232,306,310,315,448,451,455,466,470,682,685,689,696,806,810,813,817,828,832,865,869,879,885,891,897,903,907,919,934,946,962,971,975,998,1003,1006,1009],[10,11,13],"h1",{"id":12},"how-to-write-tree-shakeable-library-exports","How to Write Tree-Shakeable Library Exports",[15,16,17,18,23,24,28,29,33,34,37],"p",{},"This guide addresses library design within ",[19,20,22],"a",{"href":21},"\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002F","Tree Shaking and Dead Code Elimination",", part of ",[19,25,27],{"href":26},"\u002Fjavascript-bundle-optimization-code-splitting\u002F","JavaScript Bundle Optimization & Code Splitting",". Tree shaking is a contract between library authors and bundlers: bundlers remove code they can prove is unused, and authors write code in shapes that make the proof possible. Packaging matters — ESM output, ",[30,31,32],"code",{},"sideEffects",", ",[30,35,36],{},"exports"," — but no amount of packaging can rescue an API designed around a single object that holds everything.",[15,39,40,41,44],{},"The classic anti-pattern is the \"utility object\": ",[30,42,43],{},"export default { format, parse, add, subtract, ... }",". Any import of the default export references the object, the object references every function, and everything ships. The tree-shakeable alternative is independent named exports, each a standalone function or component that does nothing until called.",[15,46,47],{},[48,49,55,56,55,63,55,67,55,70,55,79,55,85,55,94,55,100,55,105,55,110,55,113,55,116,55,119,55,122,55,125,55,128,55,133,55,137,55,139,55,143,55,145,55,148,55,150,55,153,55,155,55],"svg",{"viewBox":50,"width":51,"role":52,"ariaLabel":53,"style":54},"0 0 760 228","100%","img","Comparison of a library exposing an object of methods with one exposing independent named exports, in terms of what consumers ship.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[57,58],"rect",{"className":59,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],"svg-canvas","0","#ffffff",[64,65,66],"title",{},"Object API vs named exports",[68,69,53],"desc",{},[57,71],{"x":72,"y":72,"width":73,"height":74,"rx":75,"fill":76,"stroke":77,"style":78},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[80,81,66],"text",{"x":82,"y":83,"fill":77,"style":84},"28.0","34.0","font-size:16px;font-weight:700",[57,86],{"x":82,"y":87,"width":88,"height":89,"rx":90,"fill":91,"stroke":92,"style":93},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[80,95,99],{"x":96,"y":97,"fill":77,"style":98},"42.0","82.0","font-size:14px;font-weight:700","export default { format, parse, ... }",[80,101,104],{"x":96,"y":102,"fill":77,"style":103},"108.0","font-size:12.5px;font-weight:700","•",[80,106,109],{"x":87,"y":102,"fill":77,"style":107,"textAnchor":108},"font-size:12.5px","start","One object references every function",[80,111,104],{"x":96,"y":112,"fill":77,"style":103},"132.0",[80,114,115],{"x":87,"y":112,"fill":77,"style":107,"textAnchor":108},"Using format ships all 60 helpers",[80,117,104],{"x":96,"y":118,"fill":77,"style":103},"156.0",[80,120,121],{"x":87,"y":118,"fill":77,"style":107,"textAnchor":108},"Methods can be reached dynamically",[80,123,104],{"x":96,"y":124,"fill":77,"style":103},"180.0",[80,126,127],{"x":87,"y":124,"fill":77,"style":107,"textAnchor":108},"Bundler cannot prove anything unused",[57,129],{"x":130,"y":87,"width":88,"height":89,"rx":90,"fill":131,"stroke":131,"style":132},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[80,134,136],{"x":135,"y":97,"fill":77,"style":98},"406.0","export function format() ...",[80,138,104],{"x":135,"y":102,"fill":77,"style":103},[80,140,142],{"x":141,"y":102,"fill":77,"style":107,"textAnchor":108},"420.0","Each function stands alone",[80,144,104],{"x":135,"y":112,"fill":77,"style":103},[80,146,147],{"x":141,"y":112,"fill":77,"style":107,"textAnchor":108},"Using format ships format and its deps",[80,149,104],{"x":135,"y":118,"fill":77,"style":103},[80,151,152],{"x":141,"y":118,"fill":77,"style":107,"textAnchor":108},"Static imports only",[80,154,104],{"x":135,"y":124,"fill":77,"style":103},[80,156,157],{"x":141,"y":124,"fill":77,"style":107,"textAnchor":108},"Unused exports removed",[159,160,162],"h2",{"id":161},"rapid-diagnosis","Rapid Diagnosis",[164,165,166,174,180,194],"ul",{},[167,168,169,173],"li",{},[170,171,172],"strong",{},"Look for default-exported objects or classes with many static methods."," These are the most common non-shakeable shapes.",[167,175,176,179],{},[170,177,178],{},"Look for module-scope work."," Precomputed tables, registrations, polyfills and environment checks run on import.",[167,181,182,185,186,189,190,193],{},[170,183,184],{},"Look for internal \"kitchen sink\" imports."," A function in ",[30,187,188],{},"format.js"," importing a large ",[30,191,192],{},"constants.js"," that also holds unrelated data drags that data along.",[167,195,196,199],{},[170,197,198],{},"Test as a consumer."," Import one function in a fixture and inspect the bundle.",[159,201,203],{"id":202},"root-cause-analysis","Root Cause Analysis",[15,205,206,209],{},[170,207,208],{},"1. Aggregating objects."," Objects and classes are values; bundlers do not remove individual properties or methods from them.",[15,211,212,215,216,219],{},[170,213,214],{},"2. Method chaining APIs."," Fluent APIs like ",[30,217,218],{},"moment().add().format()"," require all methods on the prototype, so all are shipped.",[15,221,222,225],{},[170,223,224],{},"3. Plugin registration at import time."," Libraries that auto-register locales, adapters or components on import force inclusion.",[15,227,228,231],{},[170,229,230],{},"4. Shared internal modules that mix concerns."," One big internal module imported by everything defeats fine-grained shaking.",[15,233,234],{},[48,235,55,238,55,241,55,244,55,246,55,249,55,251,55,258,55,265,55,270,55,274,55,278,55,282,55,285,55,290,55,294,55,298,55,302,55],{"viewBox":236,"width":51,"role":52,"ariaLabel":237,"style":54},"0 0 760 194","Bar chart of bytes a consumer ships to format one date with four library API styles.",[57,239],{"className":240,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],[64,242,243],{},"Consumer cost of using one date-formatting function",[68,245,237],{},[57,247],{"x":72,"y":72,"width":73,"height":248,"rx":75,"fill":76,"stroke":77,"style":78},"192",[80,250,243],{"x":82,"y":83,"fill":77,"style":84},[80,252,257],{"x":253,"y":254,"fill":77,"style":255,"textAnchor":256},"266.3","70.0","font-size:13px","end","Chained object API",[57,259],{"x":260,"y":87,"width":261,"height":262,"rx":263,"fill":91,"stroke":92,"style":264},"278.3","389.7","19","3","fill-opacity:0.7;stroke-opacity:0.9",[80,266,269],{"x":267,"y":254,"fill":77,"style":268},"674.0","font-size:12px;font-weight:600","72KB",[80,271,273],{"x":253,"y":272,"fill":77,"style":255,"textAnchor":256},"101.0","Default-export utility object",[57,275],{"x":260,"y":276,"width":277,"height":262,"rx":263,"fill":91,"stroke":92,"style":264},"87.0","221.9",[80,279,281],{"x":280,"y":272,"fill":77,"style":268},"506.2","41KB",[80,283,284],{"x":253,"y":112,"fill":77,"style":255,"textAnchor":256},"Named exports, shared constants module",[57,286],{"x":260,"y":287,"width":288,"height":262,"rx":263,"fill":131,"stroke":131,"style":289},"118.0","65.0","fill-opacity:0.55;stroke-opacity:0.9",[80,291,293],{"x":292,"y":112,"fill":77,"style":268},"349.2","12KB",[80,295,297],{"x":253,"y":296,"fill":77,"style":255,"textAnchor":256},"163.0","Named exports, split internals",[57,299],{"x":260,"y":300,"width":301,"height":262,"rx":263,"fill":131,"stroke":131,"style":289},"149.0","16.2",[80,303,305],{"x":304,"y":296,"fill":77,"style":268},"300.5","3KB",[159,307,309],{"id":308},"step-by-step-resolution","Step-by-Step Resolution",[311,312,314],"h3",{"id":313},"_1-export-functions-and-components-by-name","1. Export functions and components by name",[316,317,322],"pre",{"className":318,"code":319,"language":320,"meta":321,"style":321},"language-typescript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Before\nconst utils = { format, parse, addDays, subDays \u002F* ...60 more *\u002F };\nexport default utils;\n\n\u002F\u002F After: each export is independently removable.\nexport { format } from '.\u002Fformat';\nexport { parse } from '.\u002Fparse';\nexport { addDays, subDays } from '.\u002Farithmetic';\n\u002F\u002F trade-off: named exports change the import style for consumers (no more\n\u002F\u002F utils.format). Offer a codemod or keep a deprecated default export for one\n\u002F\u002F major version — but mark it clearly as the non-shakeable path.\n","typescript","",[30,323,324,333,357,369,376,382,400,415,430,436,442],{"__ignoreMap":321},[325,326,329],"span",{"class":327,"line":328},"line",1,[325,330,332],{"class":331},"sjfSM","\u002F\u002F Before\n",[325,334,336,340,344,347,351,354],{"class":327,"line":335},2,[325,337,339],{"class":338},"sPARh","const",[325,341,343],{"class":342},"sPXB4"," utils",[325,345,346],{"class":338}," =",[325,348,350],{"class":349},"saISM"," { format, parse, addDays, subDays ",[325,352,353],{"class":331},"\u002F* ...60 more *\u002F",[325,355,356],{"class":349}," };\n",[325,358,360,363,366],{"class":327,"line":359},3,[325,361,362],{"class":338},"export",[325,364,365],{"class":338}," default",[325,367,368],{"class":349}," utils;\n",[325,370,372],{"class":327,"line":371},4,[325,373,375],{"emptyLinePlaceholder":374},true,"\n",[325,377,379],{"class":327,"line":378},5,[325,380,381],{"class":331},"\u002F\u002F After: each export is independently removable.\n",[325,383,385,387,390,393,397],{"class":327,"line":384},6,[325,386,362],{"class":338},[325,388,389],{"class":349}," { format } ",[325,391,392],{"class":338},"from",[325,394,396],{"class":395},"sZ8jY"," '.\u002Fformat'",[325,398,399],{"class":349},";\n",[325,401,403,405,408,410,413],{"class":327,"line":402},7,[325,404,362],{"class":338},[325,406,407],{"class":349}," { parse } ",[325,409,392],{"class":338},[325,411,412],{"class":395}," '.\u002Fparse'",[325,414,399],{"class":349},[325,416,418,420,423,425,428],{"class":327,"line":417},8,[325,419,362],{"class":338},[325,421,422],{"class":349}," { addDays, subDays } ",[325,424,392],{"class":338},[325,426,427],{"class":395}," '.\u002Farithmetic'",[325,429,399],{"class":349},[325,431,433],{"class":327,"line":432},9,[325,434,435],{"class":331},"\u002F\u002F trade-off: named exports change the import style for consumers (no more\n",[325,437,439],{"class":327,"line":438},10,[325,440,441],{"class":331},"\u002F\u002F utils.format). Offer a codemod or keep a deprecated default export for one\n",[325,443,445],{"class":327,"line":444},11,[325,446,447],{"class":331},"\u002F\u002F major version — but mark it clearly as the non-shakeable path.\n",[15,449,450],{},"Expected outcome: consumers pay only for the functions they import.",[311,452,454],{"id":453},"_2-prefer-functional-apis-to-chaining","2. Prefer functional APIs to chaining",[15,456,457,458,461,462,465],{},"Replace ",[30,459,460],{},"lib(value).add(1, 'day').format('YYYY')"," with ",[30,463,464],{},"format(addDays(value, 1), 'yyyy')",". Functional composition lets each function be shaken independently; chaining forces the whole prototype.",[311,467,469],{"id":468},"_3-move-work-out-of-module-scope","3. Move work out of module scope",[316,471,473],{"className":318,"code":472,"language":320,"meta":321,"style":321},"\u002F\u002F Before: built on import, whether or not anyone formats anything.\nconst MONTH_NAMES = buildMonthTable(allLocales);\nexport function formatMonth(d: Date, locale: string) { return MONTH_NAMES[locale][d.getMonth()]; }\n\n\u002F\u002F After: built lazily, once, for the locale actually used.\nconst cache = new Map\u003Cstring, string[]>();\nexport function formatMonth(d: Date, locale: string) {\n  if (!cache.has(locale)) cache.set(locale, buildMonthNames(locale));\n  return cache.get(locale)![d.getMonth()];\n}\n\u002F\u002F trade-off: the first call per locale pays the build cost. For hot paths,\n\u002F\u002F let consumers warm the cache explicitly at startup if they want.\n",[30,474,475,480,496,546,550,555,583,610,642,666,671,676],{"__ignoreMap":321},[325,476,477],{"class":327,"line":328},[325,478,479],{"class":331},"\u002F\u002F Before: built on import, whether or not anyone formats anything.\n",[325,481,482,484,487,489,493],{"class":327,"line":335},[325,483,339],{"class":338},[325,485,486],{"class":342}," MONTH_NAMES",[325,488,346],{"class":338},[325,490,492],{"class":491},"smZ65"," buildMonthTable",[325,494,495],{"class":349},"(allLocales);\n",[325,497,498,500,503,506,509,513,516,519,521,524,526,529,532,535,537,540,543],{"class":327,"line":359},[325,499,362],{"class":338},[325,501,502],{"class":338}," function",[325,504,505],{"class":491}," formatMonth",[325,507,508],{"class":349},"(",[325,510,512],{"class":511},"sQw3B","d",[325,514,515],{"class":338},":",[325,517,518],{"class":511}," Date",[325,520,33],{"class":349},[325,522,523],{"class":511},"locale",[325,525,515],{"class":338},[325,527,528],{"class":342}," string",[325,530,531],{"class":349},") { ",[325,533,534],{"class":338},"return",[325,536,486],{"class":342},[325,538,539],{"class":349},"[locale][d.",[325,541,542],{"class":491},"getMonth",[325,544,545],{"class":349},"()]; }\n",[325,547,548],{"class":327,"line":371},[325,549,375],{"emptyLinePlaceholder":374},[325,551,552],{"class":327,"line":378},[325,553,554],{"class":331},"\u002F\u002F After: built lazily, once, for the locale actually used.\n",[325,556,557,559,562,564,567,570,573,576,578,580],{"class":327,"line":384},[325,558,339],{"class":338},[325,560,561],{"class":342}," cache",[325,563,346],{"class":338},[325,565,566],{"class":338}," new",[325,568,569],{"class":491}," Map",[325,571,572],{"class":349},"\u003C",[325,574,575],{"class":342},"string",[325,577,33],{"class":349},[325,579,575],{"class":342},[325,581,582],{"class":349},"[]>();\n",[325,584,585,587,589,591,593,595,597,599,601,603,605,607],{"class":327,"line":402},[325,586,362],{"class":338},[325,588,502],{"class":338},[325,590,505],{"class":491},[325,592,508],{"class":349},[325,594,512],{"class":511},[325,596,515],{"class":338},[325,598,518],{"class":511},[325,600,33],{"class":349},[325,602,523],{"class":511},[325,604,515],{"class":338},[325,606,528],{"class":342},[325,608,609],{"class":349},") {\n",[325,611,612,615,618,621,624,627,630,633,636,639],{"class":327,"line":417},[325,613,614],{"class":338},"  if",[325,616,617],{"class":349}," (",[325,619,620],{"class":338},"!",[325,622,623],{"class":349},"cache.",[325,625,626],{"class":491},"has",[325,628,629],{"class":349},"(locale)) cache.",[325,631,632],{"class":491},"set",[325,634,635],{"class":349},"(locale, ",[325,637,638],{"class":491},"buildMonthNames",[325,640,641],{"class":349},"(locale));\n",[325,643,644,647,650,653,656,658,661,663],{"class":327,"line":432},[325,645,646],{"class":338},"  return",[325,648,649],{"class":349}," cache.",[325,651,652],{"class":491},"get",[325,654,655],{"class":349},"(locale)",[325,657,620],{"class":338},[325,659,660],{"class":349},"[d.",[325,662,542],{"class":491},[325,664,665],{"class":349},"()];\n",[325,667,668],{"class":327,"line":438},[325,669,670],{"class":349},"}\n",[325,672,673],{"class":327,"line":444},[325,674,675],{"class":331},"\u002F\u002F trade-off: the first call per locale pays the build cost. For hot paths,\n",[325,677,679],{"class":327,"line":678},12,[325,680,681],{"class":331},"\u002F\u002F let consumers warm the cache explicitly at startup if they want.\n",[15,683,684],{},"Expected outcome: importing the module costs nothing until functions are called, and unused locale data never ships.",[311,686,688],{"id":687},"_4-make-plugins-explicit","4. Make plugins explicit",[15,690,691,692,695],{},"Instead of auto-registering, export plugins and let consumers pass them in: ",[30,693,694],{},"createFormatter({ locales: [enGB, de] })",". Only the plugins referenced are bundled.",[15,697,698],{},[48,699,55,702,55,705,55,708,55,710,55,713,55,715,55,720,55,726,55,731,55,734,55,738,55,742,55,745,55,749,55,753,55,756,55,760,55,764,55,771,55,775,55,779,55,784,55,787,55,790,55,793,55,796,55,799,55,802,55],{"viewBox":700,"width":51,"role":52,"ariaLabel":701,"style":54},"0 0 760 318","Four design rules for library APIs that bundlers can tree-shake effectively.",[57,703],{"className":704,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],[64,706,707],{},"Tree-shakeable API checklist",[68,709,701],{},[57,711],{"x":72,"y":72,"width":73,"height":712,"rx":75,"fill":76,"stroke":77,"style":78},"316",[80,714,707],{"x":82,"y":83,"fill":77,"style":84},[57,716],{"x":717,"y":87,"width":718,"height":719,"rx":90,"fill":131,"stroke":131,"style":132},"72.0","660.0","51.0",[80,721,725],{"x":722,"y":723,"fill":77,"style":724,"textAnchor":108},"86.0","77.0","font-size:13px;font-weight:700","Named exports only",[80,727,730],{"x":722,"y":728,"fill":77,"style":729,"textAnchor":108},"94.0","font-size:12px","No aggregating default objects or classes with static method collections",[57,732],{"x":717,"y":733,"width":718,"height":719,"rx":90,"fill":131,"stroke":131,"style":132},"119.0",[80,735,737],{"x":722,"y":736,"fill":77,"style":724,"textAnchor":108},"140.0","Functional composition over chaining",[80,739,741],{"x":722,"y":740,"fill":77,"style":729,"textAnchor":108},"157.0","Each operation is its own importable function",[57,743],{"x":717,"y":744,"width":718,"height":719,"rx":90,"fill":131,"stroke":131,"style":132},"182.0",[80,746,748],{"x":722,"y":747,"fill":77,"style":724,"textAnchor":108},"203.0","Nothing happens on import",[80,750,752],{"x":722,"y":751,"fill":77,"style":729,"textAnchor":108},"220.0","Lazy tables and explicit plugin registration",[57,754],{"x":717,"y":755,"width":718,"height":719,"rx":90,"fill":131,"stroke":131,"style":132},"245.0",[80,757,759],{"x":722,"y":758,"fill":77,"style":724,"textAnchor":108},"266.0","Small internal modules",[80,761,763],{"x":722,"y":762,"fill":77,"style":729,"textAnchor":108},"283.0","Shared constants split by feature so data follows usage",[327,765],{"x1":766,"y1":767,"x2":766,"y2":768,"stroke":77,"strokeWidth":769,"style":770},"43.0","95.5","130.5","1.5","stroke-opacity:0.3",[327,772],{"x1":766,"y1":773,"x2":766,"y2":774,"stroke":77,"strokeWidth":769,"style":770},"158.5","193.5",[327,776],{"x1":766,"y1":777,"x2":766,"y2":778,"stroke":77,"strokeWidth":769,"style":770},"221.5","256.5",[780,781],"circle",{"cx":766,"cy":782,"r":783,"fill":131},"81.5","13",[80,785,72],{"x":766,"y":722,"fill":62,"style":724,"textAnchor":786},"middle",[780,788],{"cx":766,"cy":789,"r":783,"fill":131},"144.5",[80,791,792],{"x":766,"y":300,"fill":62,"style":724,"textAnchor":786},"2",[780,794],{"cx":766,"cy":795,"r":783,"fill":131},"207.5",[80,797,263],{"x":766,"y":798,"fill":62,"style":724,"textAnchor":786},"212.0",[780,800],{"cx":766,"cy":801,"r":783,"fill":131},"270.5",[80,803,805],{"x":766,"y":804,"fill":62,"style":724,"textAnchor":786},"275.0","4",[159,807,809],{"id":808},"verification","Verification",[15,811,812],{},"Write consumer fixtures that import one function each and assert their bundle sizes in CI. Compare fixture size with the function's own source size plus its genuine dependencies — large discrepancies mean something else is being pulled in. Review new exports in code review against the checklist above.",[159,814,816],{"id":815},"worked-example-migrating-a-formatting-library","Worked Example: Migrating a Formatting Library",[15,818,819,820,823,824,827],{},"An internal formatting library exposed ",[30,821,822],{},"export default class Fmt"," with forty static methods and auto-loaded twelve locales at import. Every application that formatted a single currency value shipped 68KB. The team released a new major version with named functional exports, lazy locale tables loaded by explicit import (",[30,825,826],{},"import { de } from '@acme\u002Ffmt\u002Flocales\u002Fde'","), and a codemod for consumers. Applications using two or three functions dropped to 3–6KB from the library; the codemod migrated most call sites automatically, and the deprecated default export remained available for one release with a console warning in development.",[159,829,831],{"id":830},"common-mistakes","Common Mistakes",[164,833,834,840,853,859],{},[167,835,836,839],{},[170,837,838],{},"Re-exporting everything from an index that also runs setup code."," Keep the index pure.",[167,841,842,845,846,849,850,852],{},[170,843,844],{},"Using enums or large constant objects shared across features."," TypeScript ",[30,847,848],{},"enum","s compile to objects; prefer ",[30,851,339],{}," unions or per-feature constants.",[167,854,855,858],{},[170,856,857],{},"Class-based APIs for stateless helpers."," If a class has no instance state, it should be functions.",[167,860,861,864],{},[170,862,863],{},"Testing only with your own bundler."," Check output with at least webpack and Rollup\u002FVite; their analyses differ.",[159,866,868],{"id":867},"edge-cases-in-real-libraries","Edge Cases in Real Libraries",[15,870,871,874,875,878],{},[170,872,873],{},"Framework components with static registration."," Web component libraries traditionally call ",[30,876,877],{},"customElements.define()"," at import time — a side effect by design. Offer two entry points: one that defines elements (for convenience) and one that exports classes for consumers to define selectively.",[15,880,881,884],{},[170,882,883],{},"Global CSS and themes."," A component library whose index imports a global theme stylesheet makes every import pull it in. Ship the theme as a separate, explicitly imported file, and keep per-component styles next to their components.",[15,886,887,890],{},[170,888,889],{},"Polyfill-dependent utilities."," If a function needs a polyfill, do not import the polyfill at module scope; document the requirement and let the application decide. Otherwise every consumer ships the polyfill, even in browsers that do not need it.",[15,892,893,896],{},[170,894,895],{},"Configuration singletons."," Libraries often keep a module-level config object that functions read. That is fine — it is a value, not work — but avoid populating it with defaults computed from heavy imports. Defaults should be cheap literals.",[15,898,899,902],{},[170,900,901],{},"Testing the contract."," Shakeability regresses silently when someone adds a convenient module-level call. A consumer fixture that imports one export and asserts the output size, run in the library's CI, catches it on the pull request that introduces it — the only point at which the fix is cheap.",[159,904,906],{"id":905},"faq","FAQ",[908,909,912,916],"details",{"className":910},[911],"faq-item",[913,914,915],"summary",{},"Are classes always bad for tree shaking?",[15,917,918],{},"No. A class used as a unit — a component, a client — is fine; consumers who import it need all of it. Classes become a problem when they act as containers for unrelated static utilities, or when their definition has side effects (decorators that register, static initialisers that compute).",[908,920,922,925],{"className":921},[911],[913,923,924],{},"Do re-export indexes cost anything if done right?",[15,926,927,928,930,931,933],{},"With pure modules and ",[30,929,32],{}," declared, modern bundlers follow re-exports and drop unused ones, so the cost is near zero in production. They can still slow development servers in very large libraries; providing per-feature entry points in ",[30,932,36],{}," gives consumers a fast path.",[908,935,937,940],{"className":936},[911],[913,938,939],{},"How do TypeScript namespaces affect shaking?",[15,941,942,945],{},[30,943,944],{},"namespace"," declarations compile to objects populated by immediately invoked functions, which bundlers cannot shake. Use ES modules instead of namespaces for any runtime code.",[908,947,949,952],{"className":948},[911],[913,950,951],{},"Should a library provide both a namespace import and named exports?",[15,953,954,957,958,961],{},[30,955,956],{},"import * as lib from 'lib'"," works with named exports and is still shakeable when the namespace is only accessed with static property names (",[30,959,960],{},"lib.format","). It becomes non-shakeable when the namespace object is passed around or accessed dynamically. Document named imports as the recommended style and avoid APIs that encourage passing the namespace as a value.",[908,963,965,968],{"className":964},[911],[913,966,967],{},"How do I measure whether my library is shakeable?",[15,969,970],{},"Build a fixture per major export with each common bundler and record the output size. A shakeable library shows fixture sizes close to each export's own code plus its true dependencies; a non-shakeable one shows similar large sizes for every fixture, because the whole library comes along each time.",[159,972,974],{"id":973},"related","Related",[164,976,977,984,991],{},[167,978,979,983],{},[19,980,982],{"href":981},"\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fusing-pure-annotations-for-side-effect-free-calls\u002F","Using PURE annotations for side-effect-free calls"," — when exports must be created by calls.",[167,985,986,990],{},[19,987,989],{"href":988},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fvite-and-rollup-build-optimization\u002Fbuilding-tree-shakeable-libraries-with-vite-library-mode\u002F","Building tree-shakeable libraries with Vite library mode"," — packaging the result.",[167,992,993,997],{},[19,994,996],{"href":995},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fmodern-module-formats-esm-vs-commonjs\u002Fshipping-esm-only-packages\u002F","Shipping ESM-only packages"," — the module format these patterns depend on.",[999,1000,1002],"script",{"type":1001},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Write Tree-Shakeable Library Exports\",\n  \"description\": \"Source-level patterns for library authors that let applications ship only the functions and components they import.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Export functions and components by name\",\n      \"text\": \"\u002F\u002F After: each export is independently removable.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Prefer functional APIs to chaining\",\n      \"text\": \"Replace lib(value).add(1, 'day').format('YYYY') with format(addDays(value, 1), 'yyyy').\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Move work out of module scope\",\n      \"text\": \"\u002F\u002F After: built lazily, once, for the locale actually used.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Make plugins explicit\",\n      \"text\": \"Instead of auto-registering, export plugins and let consumers pass them in: createFormatter({ locales: [enGB, de] }).\"\n    }\n  ]\n}\n",[999,1004,1005],{"type":1001},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Write Tree-Shakeable Library Exports\",\n  \"description\": \"Source-level patterns for library authors that let applications ship only the functions and components they import.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports\u002F\"\n  }\n}\n",[999,1007,1008],{"type":1001},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"JavaScript Bundle Optimization & Code Splitting\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Tree Shaking and Dead Code Elimination\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Writing Tree-Shakeable Library Exports\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports\u002F\"\n    }\n  ]\n}\n",[1010,1011,1012],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}",{"title":321,"searchDepth":335,"depth":335,"links":1014},[1015,1016,1017,1023,1024,1025,1026,1027,1028],{"id":161,"depth":335,"text":162},{"id":202,"depth":335,"text":203},{"id":308,"depth":335,"text":309,"children":1018},[1019,1020,1021,1022],{"id":313,"depth":359,"text":314},{"id":453,"depth":359,"text":454},{"id":468,"depth":359,"text":469},{"id":687,"depth":359,"text":688},{"id":808,"depth":335,"text":809},{"id":815,"depth":335,"text":816},{"id":830,"depth":335,"text":831},{"id":867,"depth":335,"text":868},{"id":905,"depth":335,"text":906},{"id":973,"depth":335,"text":974},"Source-level patterns for library authors that let applications ship only the functions and components they import.","md",{"slug":1032,"type":1033,"breadcrumb":1034,"datePublished":1042,"dateModified":1042},"writing-tree-shakeable-library-exports","article",[1035,1038,1039,1040],{"name":1036,"url":1037},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1041},"\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports\u002F","2026-10-06","\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports",{"title":5,"description":1045},"Design library exports that bundlers can eliminate when unused: named ESM exports, no top-level side effects, no namespace objects, and per-feature entry points.","javascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwriting-tree-shakeable-library-exports\u002Findex","jgXB7nFRxBetarIleCtnPV5yYSp0ElStZqJ2pemokNM",[1049,1053],{"title":1050,"path":1051,"stem":1052},"Why Barrel Files Break Tree Shaking","\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwhy-barrel-files-break-tree-shaking","javascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwhy-barrel-files-break-tree-shaking\u002Findex",{"title":1054,"path":1055,"stem":1056},"Vite & Rollup Build Optimization","\u002Fjavascript-bundle-optimization-code-splitting\u002Fvite-and-rollup-build-optimization","javascript-bundle-optimization-code-splitting\u002Fvite-and-rollup-build-optimization\u002Findex",1791308077611]