Performance
Learn how to optimize your lit-jsx applications for maximum performance.
Zero Runtime Overhead
The most important performance feature of lit-jsx is that it has zero runtime overhead. All JSX is transformed to native Lit templates at compile time.
// Your JSX code
<div class="container">
<h1>{title}</h1>
<p>{description}</p>
</div>
// Compiles directly to
html`
<div class="container">
<h1>${title}</h1>
<p>${description}</p>
</div>
`No JSX runtime library is included in your bundle. The transformation happens entirely during the build process.
Static Templates
Lit templates are parsed once and reused. lit-jsx preserves this optimization:
// This template is parsed once
function Greeting({ name }) {
return <div>Hello, {name}!</div>
}
// Even when called multiple times, the template structure is reused
<Greeting name="Alice" />
<Greeting name="Bob" />
<Greeting name="Charlie" />Efficient Updates
Lit only updates the dynamic parts of templates:
function Counter({ count }) {
return (
<div>
<h1>Counter</h1>
<p>Count: {count}</p>
<button>Increment</button>
</div>
)
}
// When count changes:
// - The structure (<div>, <h1>, <p>, <button>) stays the same
// - Only the {count} value is updated in the DOMConditional Rendering
Use JavaScript expressions for efficient conditional rendering:
function Message({ type, text }) {
return (
<div>
{type === 'error' && <ErrorIcon />}
{text}
</div>
)
}
// Only ErrorIcon is added/removed when type changesList Rendering
For optimal list performance, use the repeat directive with keys:
import { repeat } from 'lit/directives/repeat.js'
function TodoList({ todos }) {
return (
<ul>
{repeat(
todos,
(todo) => todo.id, // Key function
(todo) => <li>{todo.text}</li>
)}
</ul>
)
}Without keys, Lit updates all items. With keys, Lit only updates changed items.
Memoization with guard
Use the guard directive to skip expensive re-renders:
import { guard } from 'lit/directives/guard.js'
function ExpensiveComponent({ data }) {
const processData = (data) => {
// Expensive computation
return data.map(item => transform(item))
}
return (
<div>
{guard([data], () => (
<div>{processData(data)}</div>
))}
</div>
)
}
// processData only runs when data changesCache for Tab Panels
Use cache to preserve DOM for tab panels:
import { cache } from 'lit/directives/cache.js'
function TabPanel({ activeTab }) {
return (
<div>
{cache(
activeTab === 'home' ? <HomePage /> :
activeTab === 'profile' ? <ProfilePage /> :
<SettingsPage />
)}
</div>
)
}
// DOM is preserved when switching tabs
// No need to recreate the tab contentLazy Loading
Lazy load components with dynamic imports:
import { until } from 'lit/directives/until.js'
function App() {
const HeavyComponent = import('./HeavyComponent.js')
.then(m => m.default)
return (
<div>
{until(
HeavyComponent.then(Component => <Component />),
<div>Loading...</div>
)}
</div>
)
}Virtual Scrolling
For long lists, consider virtual scrolling:
import { virtualScroll } from '@arcmantle/virtualizer'
function LongList({ items }) {
return (
<div>
{virtualScroll({
items,
renderItem: (item) => <div>{item.name}</div>,
itemHeight: 50,
})}
</div>
)
}
// Only visible items are renderedProperty Bindings vs Attributes
Use property bindings for better performance with complex data:
// Slow: JSON serialization on every update
<custom-element config="${JSON.stringify(config)}" />
// Fast: Direct property assignment
<custom-element config={as.prop(config)} />Avoid Inline Functions (When It Matters)
For frequently updated components, avoid creating new functions on every render:
// Less optimal: New function on every render
function TodoItem({ todo }) {
return (
<div onclick={() => handleClick(todo.id)}>
{todo.text}
</div>
)
}
// Better: Stable function reference
function TodoItem({ todo, onClick }) {
return (
<div onclick={onClick}>
{todo.text}
</div>
)
}Note: This optimization is only important for high-frequency updates.
Bundle Size
lit-jsx has zero runtime overhead, so your bundle only contains:
- Lit library (~7KB gzipped)
- Your component code
- Directives you actually use
Compare this to React JSX:
- React library (~40KB gzipped)
- React DOM (~130KB gzipped)
- JSX runtime
Build Optimization
The Vite plugin optimizes during build:
// vite.config.ts
import { defineConfig } from 'vite'
import { litJsx } from '@arcmantle/lit-jsx/vite'
export default defineConfig({
plugins: [litJsx()],
build: {
minify: 'terser',
terserOptions: {
compress: {
dead_code: true,
drop_debugger: true,
},
},
},
})Measuring Performance
Use browser DevTools to measure performance:
function App() {
performance.mark('render-start')
const result = (
<div>
<Header />
<MainContent />
<Footer />
</div>
)
performance.mark('render-end')
performance.measure('render', 'render-start', 'render-end')
return result
}Best Practices
- Use Static Templates: Let Lit parse templates once
- Key Your Lists: Use
repeatwith keys for lists - Guard Expensive Renders: Use
guardfor heavy computations - Cache Tab Content: Use
cachefor tab panels - Property Bindings: Use
as.prop()for complex data - Lazy Load: Split code with dynamic imports
- Virtual Scroll: Use virtual scrolling for long lists
Performance Checklist
- [ ] Using property bindings for complex data
- [ ] Keys on list items with
repeatdirective - [ ]
guarddirective for expensive computations - [ ]
cachedirective for tab panels - [ ] Lazy loading heavy components
- [ ] Virtual scrolling for long lists
- [ ] Avoiding unnecessary re-renders
- [ ] Measuring with DevTools
Next Steps
- Check out the API Reference
- Explore the source code on GitHub
- Join the community discussions