Skip to main content

Bundle Sizes

All sizes are minified + brotli compressed:

griffo (Core)

7.11 kBVanilla JS, works with any framework

griffo/react

8.23 kBReact wrapper with lifecycle management

griffo/motion

13.78 kBMotion variant system (excludes Motion itself)

griffo/morph

7.95 kBStandalone 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.

Auto-split Performance

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>

Animation Performance

Use Transform Properties

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>

Measuring Performance

Lighthouse Audits

  1. Run Lighthouse in Chrome DevTools
  2. Check “Performance” score
  3. Look for:
    • Total Blocking Time (TBT)
    • Cumulative Layout Shift (CLS)
    • Largest Contentful Paint (LCP)

Chrome DevTools Performance Tab

  1. Open DevTools → Performance
  2. Record while page loads
  3. 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

1

Enable compression

Use brotli or gzip compression for static assets
2

Preload critical fonts

Add <link rel="preload"> for fonts used in split text
3

Use tree-shaking

Import from specific entry points (griffo, griffo/react, etc.)
4

Wait for fonts

Enable waitForFonts (default in React/Motion)
5

Limit split types

Only split by types you’re actually animating
6

Revert after animations

Clean up with revert() or revertOnComplete
7

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

Kerning

How kerning works

Accessibility

Screen reader support

Build docs developers (and LLMs) love