Performance Tips

How to keep Byteion-powered interfaces fast and responsive.

Overview

Byteion core is lightweight by default: static components are HTML and CSS, while interactive behaviour is opt-in through @byteion/js or framework packages (@byteion/react, @byteion/vue). A simple page of buttons, cards, and text has near-zero overhead.

However, performance still depends on how you compose pages, render data, mount hidden UI, load assets, and configure Tailwind. A dashboard with 10,000 table rows, 20 modals mounted on page load, and undispatched search handlers will be slow regardless of the design system.

This guide covers practical performance patterns for real Byteion applications — not theoretical benchmarks.

Performance principles

1
Render less

Only render what the user can see. Paginate tables, virtualize lists, and avoid rendering off-screen content.

2
Mount late

Lazy-mount modals, drawers, tabs, and popovers. Do not render hidden content on initial page load.

3
Fetch intentionally

Fetch only the data needed for the current view. Use server-side pagination, debounced search, and avoid over-fetching.

4
Animate with CSS

Use CSS transitions and Tailwind animate-* utilities for UI motion. Avoid JavaScript-driven animations for simple state changes.

5
Keep assets small

Optimize images, use loading="lazy", keep Tailwind content paths tight, and avoid importing demo code in production bundles.

6
Keep Tailwind output clean

Accurate content paths prevent bloated CSS. Avoid broad safelists that include unused utility variants.

7
Test with production data

Demo data hides performance problems. Test tables with 10,000 rows, forms with 50 fields, and modals with heavy content.

Core rules

Paginate or virtualize large tables and lists

Render only the visible page of rows. Use server-side pagination for datasets larger than 500 rows. For very large lists, consider virtualization — but start with pagination first.

Use server-side pagination for large datasets

Fetching 25 rows from the server is faster than fetching 10,000 and paginating client-side. Server-side pagination also reduces memory usage and initial load time.

Lazy-mount heavy modals, drawers, tabs, and popovers

Do not render modal content until the user triggers the open action. Use v-if (Vue) or conditional rendering (React) to keep the DOM clean. A hidden modal with complex forms still executes JavaScript and creates DOM nodes.

Debounce or throttle search and filters

Triggering an API call on every keystroke floods the network and UI. Use a debounce of 250–400ms for search inputs. For rapid interactions like sliders, use throttling instead.

Use CSS transitions and animations for UI motion

Tailwind's transition-* and animate-* utilities are GPU-accelerated and avoid JavaScript thread contention. Reserve JavaScript animations for complex choreography.

Avoid layout thrashing

Repeatedly reading and writing to the DOM (e.g., reading offsetHeight then setting style.width in a loop) forces synchronous reflows. Batch reads separately from writes.

Use skeletons instead of blocking the page

Show Skeleton rows or placeholders while data loads. This prevents layout shift and gives users immediate feedback that content is coming.

Optimize images with width, height, and lazy loading

Always set explicit width and height on images to prevent layout shift. Use loading="lazy" for below-the-fold images. Serve images in modern formats (WebP, AVIF).

Do not render hidden modal content on initial page load

If you have a table of 100 rows and each row has a hidden modal for editing, that is 100 hidden modal instances in the DOM. Use a single modal component and populate it dynamically when the user clicks "Edit".

Avoid importing demo and example code into production bundles

Demo pages, storybook stories, and example data files should not be importable from production entry points. Use separate directories or build exclusions.

Keep Tailwind content paths accurate

A broad content path like ./src/**/*.{html,js,ts,jsx,tsx,vue} is fine. Adding a broad safelist with every variant is not. Only safelist classes you genuinely use outside of scanned files.

Avoid duplicate CSS imports

Importing byteion-theme.css in multiple entry points or components can duplicate the entire theme in your CSS bundle. Import it once at the application root.

Performance budget guidance

ScenarioRecommended limit
Table rows before pagination50–100 rows per page for admin tables
Rows per page (admin tables)25–50 rows
Search debounce delay250–400ms
Modal lazy-mount thresholdAny modal with form fields or remote data
UI transition duration150–250ms for common transitions
Table loading stateSkeleton rows, not a full-page spinner
Large dataset strategyServer-side pagination + search + sort

Examples

A. Bad table: 1000+ rows, hidden modals per row, no debounce

This pattern renders every row at once, creates a hidden modal instance for every row, and fires an API call on every keystroke. It will be slow with more than a few hundred rows.

<!-- BAD: Do not copy -->
<input type="text" oninput="search(this.value)">
<table>
  <!-- 1000+ rows rendered at once -->
  <tr>
    <td>Item 1</td>
    <td><button onclick="openEditModal(1)">Edit</button></td>
  </tr>
  <!-- Each row has its own hidden modal instance -->
  <div id="modal-1" class="hidden"><!-- modal content --></div>
  <div id="modal-2" class="hidden"><!-- modal content --></div>
  <!-- ... 998 more hidden modals -->
</table>

B. Good table page with debounced search, pagination, semantic tokens

Pagination limits rendered rows. A single modal is mounted on demand. Search is debounced. All classes use Byteion semantic tokens.

<div class="space-y-4">
  <!-- Debounced search -->
  <input type="search" placeholder="Search orders..."
         class="w-64 rounded-input border border-border bg-background px-3 py-2 text-sm text-foreground outline-none placeholder:text-muted-foreground focus:border-primary focus:ring-2 focus:ring-primary/20"
         data-debounce="300">

  <!-- Table surface -->
  <div class="overflow-hidden rounded-card border border-border bg-card shadow-card">
    <table class="w-full text-sm">
      <thead class="border-b border-border bg-muted/50">
        <tr>
          <th class="px-4 py-3 text-left font-medium text-foreground">Order</th>
          <th class="px-4 py-3 text-left font-medium text-foreground">Customer</th>
          <th class="px-4 py-3 text-left font-medium text-foreground">Amount</th>
          <th class="px-4 py-3 text-right font-medium text-foreground">Actions</th>
        </tr>
      </thead>
      <tbody class="divide-y divide-border">
        <tr class="hover:bg-muted/50">
          <td class="px-4 py-3 text-foreground">#10234</td>
          <td class="px-4 py-3 text-muted-foreground">Jane Cooper</td>
          <td class="px-4 py-3 text-foreground">$129.00</td>
          <td class="px-4 py-3 text-right">
            <button class="text-sm text-primary hover:underline" onclick="openEditModal(10234)">Edit</button>
          </td>
        </tr>
        <!-- 24 more rows rendered (25 per page) -->
      </tbody>
    </table>
  </div>

  <!-- Pagination -->
  <div class="flex items-center justify-between text-sm text-muted-foreground">
    <span>Showing 1 to 25 of 1,234 results</span>
    <div class="flex items-center gap-2">
      <button class="rounded-button border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted">Previous</button>
      <button class="rounded-button border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted">Next</button>
    </div>
  </div>
</div>

<!-- Single lazy modal instance (hidden until opened) -->
<div id="edit-modal" class="hidden">
  <!-- Populated dynamically when Edit is clicked -->
</div>

C. Good lazy modal pattern

Modal shell is rendered only when opened. Heavy data is fetched after open. The submit button shows a loading state during save.

<!-- Modal container: rendered only when open -->
<div id="edit-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-background/80">
  <div class="w-full max-w-lg rounded-card border border-border bg-card shadow-modal">
    <div class="flex items-center justify-between border-b border-border px-6 py-4">
      <h2 class="text-lg font-semibold text-foreground">Edit order</h2>
      <button onclick="closeModal()" class="rounded-button p-1 text-muted-foreground hover:bg-muted">×</button>
    </div>
    <div class="space-y-4 px-6 py-4">
      <!-- Fields populated from API response -->
    </div>
    <div class="flex items-center justify-end gap-3 border-t border-border px-6 py-4">
      <button onclick="closeModal()" class="rounded-button border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted">Cancel</button>
      <button id="save-btn" class="rounded-button bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save</button>
    </div>
  </div>
</div>

<!-- Loading state on save -->
<script>
document.getElementById('save-btn').addEventListener('click', async function() {
  this.disabled = true;
  this.innerHTML = '<svg class="h-4 w-4 animate-spin inline">...</svg> Saving…';
  try {
    await fetch('/api/orders/update', { method: 'POST', ... });
    closeModal();
  } catch {
    this.disabled = false;
    this.textContent = 'Save';
    alert('Failed to save. Please try again.');
  }
});
</script>

React and Vue notes

React

  • Avoid rendering hidden modal trees. Use a single modal component controlled by state, not one modal per row.
  • Memoize expensive rows with React.memo only when profiling shows a benefit — premature memoization adds complexity.
  • Debounce controlled inputs before triggering API calls. Use useDeferredValue or a custom debounce hook.
  • Split heavy dashboard sections with lazy loading (React.lazy + Suspense) if they are below the fold.
  • Avoid passing unstable inline objects or functions as props to many list items — this breaks memoization and causes re-renders.

Vue

  • Avoid large v-for lists without pagination or virtualization. Even with efficient reactivity, 1,000+ DOM nodes are expensive to render and re-render.
  • Use computed properties carefully for large datasets — avoid full-array filters that run on every reactive dependency change.
  • Lazy-mount modal, drawer, and tab content with v-if instead of v-show for heavy components. v-if removes the DOM entirely; v-show only hides it.
  • Avoid deep watchers on large objects. Watch specific properties instead of the entire object.

Async / data performance scenarios

Data table with 10,000 rows

Use server-side pagination (25 rows per page). Show skeleton rows during fetch. Debounce search at 300ms. Cache adjacent pages for instant back/forward navigation. Never fetch all 10,000 rows client-side.

Ecommerce product editor with many variants

Lazy-load variant data only when the user expands the "Variants" section. Use a single reusable form component for each variant row instead of cloning DOM. Paginate variant rows if there are more than 50.

ERP / POS transaction screen

Render only the current transaction. Fetch product and customer data via async search with debounce. Show skeleton placeholders for slow lookups. Avoid re-rendering the entire screen on each cart update — update only the affected row.

Dashboard analytics cards

Each metric card should fetch its own data independently. This prevents one slow API from blocking the entire dashboard. Show skeleton cards (matching the card dimensions) while data loads. Stagger requests to avoid a network spike.

Modal form with remote select fields

Fetch remote select options only when the modal opens, not on page load. Debounce remote search at 250ms. Cache options client-side if the data is small and changes infrequently. Show a loading spinner in the select while options load.

Customer search / autocomplete

Debounce at 300ms. Cancel in-flight requests when the user types again. Limit results to 10–20 items. Show a loading indicator during fetch. Highlight the matching text in results. Use aria-busy and aria-live for accessibility.

Do and Don't

Do

  • Paginate or virtualize large datasets
  • Lazy-mount heavy hidden UI (modals, drawers, tabs)
  • Debounce search and filter inputs at 250–400ms
  • Use CSS transitions for simple UI motion
  • Test with realistic production data volumes
  • Keep Tailwind content paths tight and accurate
  • Use semantic loading states (skeleton, spinner)
  • Import byteion-theme.css once at the app root

Don't

  • Render 1,000+ rows without pagination or virtualization
  • Trigger API requests on every keystroke — debounce them
  • Mount every modal on initial page load
  • Put a hidden modal inside every table row
  • Safelist huge Tailwind patterns unnecessarily
  • Ship demo-only examples in production bundles
  • Animate layout-heavy properties like top, left, width unnecessarily
  • Import byteion-theme.css in every component file

Developer tips

Run Lighthouse on production builds. Development mode includes source maps, hot reload, and uncompressed assets that hide real performance. Always measure against a production build.

Use the Browser DevTools Performance tab. Record a typical user flow (load page, search, open modal, save) and look for long tasks, forced reflows, and layout thrashing.

Test worst-case scenarios. Load a table with 10,000 rows, open a modal with 50 form fields, search with a single character that matches everything. Real users will find these paths.

Profile before adding complex memoization. Measure first. A React.memo or computed cache that is not needed adds complexity without benefit. Let profiling guide your optimization decisions.

Prefer simple architecture first. A straightforward paginated table with server-side fetching is faster and easier to maintain than a virtualized list with complex memoization. Add complexity only when profiling shows a need.

Check dark mode and theme switching. Theme changes should not cause layout jumps or full-page re-renders. The Byteion token system uses CSS variables, which update instantly without JavaScript-driven re-layout.

Keep examples copy-paste friendly. Every example on this page uses production-ready patterns. Paste them directly into a Byteion project and adjust the data source.

  • Composition Rules — efficient page structure without unnecessary wrappers
  • State System — loading, skeleton, and error states
  • Data Table — pagination and loading patterns
  • Modal — lazy mounting and form performance
  • Form Field — form performance and debounced validation
  • Skeleton — loading placeholder patterns
  • JavaScript Behaviour — JS interaction performance
  • React — React-specific performance patterns
  • Vue — Vue-specific performance patterns
  • Theming Guide — theme switching without layout jumps

Unlock Premium Patterns

Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.

Explore Pro Components