Bundle Sizes
All sizes are minified + brotli compressed:
griffo (Core) 7.11 kB Vanilla JS, works with any framework
griffo/react 8.23 kB React wrapper with lifecycle management
griffo/motion 13.78 kB Motion variant system (excludes Motion itself)
griffo/morph 7.95 kB Standalone MorphText component
Sizes measured with brotli compression (recommended for production). With gzip, add ~15-20%.
Tree-Shaking
Griffo is fully tree-shakeable when using modern bundlers (Vite, Rollup, Webpack 5+):
// Only imports core splitText (~7 kB)
import { splitText } from "griffo" ;
// Only imports React wrapper (~8 kB)
import { SplitText } from "griffo/react" ;
// Only imports Motion variant system (~14 kB)
import { SplitText } from "griffo/motion" ;
// Only imports MorphText (~8 kB)
import { MorphText } from "griffo/morph" ;
Avoid importing from package root if you only need specific exports: // ❌ May pull in more code than needed
import { splitText , SplitText } from "griffo" ;
// ✅ Import from specific entry points
import { splitText } from "griffo" ;
import { SplitText } from "griffo/react" ;
Font Loading
Wait for Fonts
Always wait for fonts before splitting to ensure accurate measurements:
// Vanilla JS
document . fonts . ready . then (() => {
const { words } = splitText ( element , { type: "words" });
});
// React (default behavior)
< SplitText waitForFonts = { true } >
< h1 > Text </ h1 >
</ SplitText >
Preload Critical Fonts
Preload fonts used in split text to avoid FOUT (Flash of Unstyled Text):
< link
rel = "preload"
href = "/fonts/inter-var.woff2"
as = "font"
type = "font/woff2"
crossorigin
/>
Font Display Strategy
@font-face {
font-family : 'Inter' ;
src : url ( '/fonts/inter-var.woff2' ) format ( 'woff2' );
font-display : swap ; /* or 'block' for critical text */
}
Best for : Body text, non-critical contentRenders fallback immediately, swaps when font loads. May cause layout shift.
Best for : Hero text, logos, headingsWaits for font (up to 3s), ensures no layout shift. Use with waitForFonts.
Best for : Non-essential textUses font if already cached, otherwise uses fallback. Avoids layout shift.
Debouncing
Control how often re-splits occur on resize:
splitText ( element , {
autoSplit: true ,
resplitDebounceMs: 150 , // Default: 100ms
});
Increase resplitDebounceMs if you notice performance issues during window resize.
Disable When Hidden
Disconnect observers when elements aren’t visible:
const result = splitText ( element , { autoSplit: true });
// Cleanup when component unmounts or hides
result . revert ();
In React:
function MyComponent ({ isVisible }) {
return isVisible ? (
< SplitText autoSplit >
< h1 > Text </ h1 >
</ SplitText >
) : null ;
}
Kerning Measurement
Isolation (Default)
By default, kerning is measured in an isolated container to avoid layout thrashing:
splitText ( element , {
type: "chars" ,
// isolateKerningMeasurement: true (default)
});
This creates a hidden <div> for measurements, avoiding forced reflows in your main layout.
Disable Kerning
For better performance when kerning isn’t critical:
splitText ( element , {
type: "chars" ,
disableKerning: true , // Skips all kerning measurement
});
Disabling kerning may cause visible spacing differences, especially with fonts that have aggressive kerning (e.g., display fonts).
Memory Management
Revert When Done
Always revert splits when animations complete:
const { chars , revert } = splitText ( element , { type: "chars" });
animate ( chars , { opacity: [ 0 , 1 ] }). finished . then (() => {
revert (); // Cleans up observers, restores original HTML
});
Auto-revert
splitText ( element , {
type: "words" ,
revertOnComplete: true ,
onSplit : ({ words }) => animate ( words , { opacity: [ 0 , 1 ] }),
});
React Cleanup
React components handle cleanup automatically on unmount:
< SplitText >
< h1 > Automatically cleaned up </ h1 >
</ SplitText >
Animate transform and opacity for best performance (GPU-accelerated):
// ✅ Good: GPU-accelerated
animate ( chars , {
opacity: [ 0 , 1 ],
transform: [ "translateY(20px)" , "translateY(0)" ],
});
// ❌ Avoid: Triggers layout
animate ( chars , {
marginTop: [ 20 , 0 ],
height: [ 0 , "auto" ],
});
Will-change
Add will-change for complex animations:
splitText ( element , {
type: "chars" ,
initialStyles: {
chars: {
willChange: "transform, opacity" ,
},
},
});
Remove will-change after animation completes to avoid memory overhead: animate ( chars , { opacity: 1 }). finished . then (() => {
chars . forEach ( char => char . style . willChange = "" );
});
Stagger Limits
Limit the number of simultaneously animating elements:
// ❌ 1000 chars animating at once
const { chars } = splitText ( element , { type: "chars" });
animate ( chars , { opacity: [ 0 , 1 ] }, { delay: stagger ( 0.01 ) });
// ✅ Split by words (fewer elements)
const { words } = splitText ( element , { type: "words" });
animate ( words , { opacity: [ 0 , 1 ] }, { delay: stagger ( 0.05 ) });
Reducing DOM Complexity
Only Split What You Animate
// ❌ Splits everything
const result = splitText ( element , { type: "chars,words,lines" });
// ✅ Only split what you need
const { words } = splitText ( element , { type: "words" });
Avoid Deep Nesting
Simplify HTML structure before splitting:
<!-- ❌ Deep nesting -->
< div >
< div >
< div >
< h1 > Text </ h1 >
</ div >
</ div >
</ div >
<!-- ✅ Flat structure -->
< h1 > Text </ h1 >
Lighthouse Audits
Run Lighthouse in Chrome DevTools
Check “Performance” score
Look for:
Total Blocking Time (TBT)
Cumulative Layout Shift (CLS)
Largest Contentful Paint (LCP)
Open DevTools → Performance
Record while page loads
Look for:
Long tasks (>50ms)
Layout thrashing
Excessive reflows
Custom Timing
performance . mark ( "split-start" );
const result = splitText ( element , { type: "chars" });
performance . mark ( "split-end" );
performance . measure ( "split-duration" , "split-start" , "split-end" );
const [ measure ] = performance . getEntriesByName ( "split-duration" );
console . log ( `Split took ${ measure . duration . toFixed ( 2 ) } ms` );
Production Checklist
Enable compression
Use brotli or gzip compression for static assets
Preload critical fonts
Add <link rel="preload"> for fonts used in split text
Use tree-shaking
Import from specific entry points (griffo, griffo/react, etc.)
Wait for fonts
Enable waitForFonts (default in React/Motion)
Limit split types
Only split by types you’re actually animating
Revert after animations
Clean up with revert() or revertOnComplete
Test on devices
Profile on low-end mobile devices, not just desktop
Bundle Analysis
Vite
npm run build -- --mode analyze
Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require ( 'webpack-bundle-analyzer' );
module . exports = {
plugins: [
new BundleAnalyzerPlugin ()
]
};
Next.js
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require ( '@next/bundle-analyzer' )({
enabled: process . env . ANALYZE === 'true' ,
});
module . exports = withBundleAnalyzer ({});
Vanilla JS Core API reference
React React lifecycle management
Accessibility Screen reader support