Lesson 8: Performance
Benchmarks, bundle size comparisons, and optimization strategies.
Lesson 8 of 8: how @gentleduck/calendar compares to other React calendar libraries, plus optimization tips.
Bundle size
We replaced react-day-picker with @gentleduck/calendar. Comparison against the major React calendar libraries:
Bundle size — all competitors
| Library | Bundle (gzipped) | Improvement |
|---|---|---|
| @gentleduck/calendar | 4.9 KB | Winner |
| react-calendar | 15.0 KB | 3.1x larger |
| react-day-picker v9 | 20.0 KB | 4.1x larger |
| react-datepicker | 32.0 KB | 6.5x larger |
| react-aria DatePicker | 45.0 KB | 9.2x larger |
Dependencies
| Library | Runtime Dependencies |
|---|---|
| @gentleduck/calendar | 0 |
| react-calendar | 0 |
| react-day-picker | 1 (date-fns) |
| react-datepicker | 3 |
| react-aria | 8 |
Render performance
Core function benchmarks (average of 2000 iterations):
| Operation | Time |
|---|---|
buildCalendarMonth | ~8 us |
buildMultiMonth(3) | ~22 us |
buildMultiMonth(12) | ~78 us |
applySelection | ~11 us |
Generate benchmarks locally:
cd packages/duck-calendar && bun run benchmarkcd packages/duck-calendar && bun run benchmarkPackage quality
| Tool | Result |
|---|---|
| publint | All good |
| attw (are-the-types-wrong) | Types correct for node10, node16, bundler |
| ESM | Pure ESM, "type": "module" |
Optimizations
These are the changes that took the core bundle from 7.0 KB down to 4.9 KB gzipped (75% smaller than react-day-picker):
- Intl.DateTimeFormat caching — formatters cached per locale+options, avoiding expensive re-instantiation.
- React.memo on day cells — prevents re-rendering 42 unchanged cells on selection change.
- Stable keyboard callback —
configRefpattern, zero recreation cost. - Pre-computed time field orders — 4 static arrays replace
Array.filter()on every keystroke. - Frozen selection constants — one
UNSELECTEDobject shared across unselected days. - Dev JSX elimination — production build uses
createElementdirectly, nojsxDEVmetadata.
Why it's fast
- Pure functions — grid building and selection allocate no closures and hold no subscriptions.
- No date library dependency —
NativeAdapteruses nativeDateandIntl.DateTimeFormat. - Tree-shakeable — import only what you need; if it's just
buildCalendarMonth, the rest goes. - Minimal React overhead — hooks use
useMemoto avoid rebuilding grids on every render. - No CSS runtime — no CSS-in-JS overhead, no stylesheet injection.
Optimization tips
- Wrap day cells in
React.memoto skip re-renders on unchanged days. - Use
numberOfMonths: 1unless you need multi-month views. - Use
fixedWeeks: falseto render fewer day cells. - Memoize the adapter instance outside the component — it's stateless.