Performance Budgets
Fast loading and smooth interactions are critical for Base MiniApps. These performance budgets define the standards your app must meet for featuring eligibility and optimal user experience.
Quality Bar RequirementMeeting these performance budgets is mandatory for featuring. Apps that don’t meet these standards will not pass the Quality Bar checklist.
| Metric | Target | Measurement |
|---|
| First Contentful Paint (FCP) | ≤ 1.2s | Time to first visible content |
| Largest Contentful Paint (LCP) | ≤ 2.5s | Time to main content loaded |
| First Input Delay (FID) | ≤ 100ms | Responsiveness to user interaction |
| Time to Interactive (TTI) | ≤ 1.5s | App ready for meaningful interaction |
| Metric | Target | Measurement |
|---|
| Cumulative Layout Shift (CLS) | ≤ 0.1 | Visual stability score |
| Total Blocking Time (TBT) | ≤ 200ms | Main thread blocking time |
| Frame Rate | ≥ 60 FPS | Smooth animations and scrolling |
| Memory Usage | ≤ 50MB | Peak JavaScript heap size |
Network Budgets
| Resource Type | Budget | Notes |
|---|
| Initial JavaScript | ≤ 200KB (gzipped) | Critical path JS bundle |
| Initial CSS | ≤ 50KB (gzipped) | Above-the-fold styles |
| First Screen Images | ≤ 300KB total | All images visible on load |
| Total Initial Payload | ≤ 600KB | Everything needed for first interaction |
Detailed Requirements
Bundle Size Optimization
// Webpack bundle analysis example
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false,
reportFilename: "bundle-report.html",
}),
],
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
},
},
},
},
};
Code Splitting Strategy
// Route-based code splitting
import { lazy, Suspense } from "react";
const HomePage = lazy(() => import("./HomePage"));
const GamePage = lazy(() => import("./GamePage"));
const ProfilePage = lazy(() => import("./ProfilePage"));
function App() {
return (
<Suspense fallback={<LoadingSkeleton />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/game" element={<GamePage />} />
<Route path="/profile" element={<ProfilePage />} />
</Routes>
</Suspense>
);
}
// Component-based code splitting
const HeavyChart = lazy(() =>
import("./HeavyChart").then((module) => ({
default: module.HeavyChart,
}))
);
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<DashboardHeader />
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart />
</Suspense>
)}
<button onClick={() => setShowChart(true)}>Show Advanced Chart</button>
</div>
);
}
Critical CSS Inlining
<!-- Inline critical CSS for above-the-fold content -->
<style>
/* Critical styles for first screen */
.app-container {
max-width: 100vw;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(
safe-area-inset-bottom
) env(safe-area-inset-left);
}
.hero-section {
min-height: 50vh;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<!-- Load non-critical CSS asynchronously -->
<link
rel="preload"
href="/styles/non-critical.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/styles/non-critical.css" /></noscript>
CSS Optimization
/* Use efficient selectors */
.button-primary {
/* Good: class selector */
}
#header .nav > ul li a {
/* Avoid: overly specific */
}
/* Minimize expensive properties */
.smooth-animation {
/* Prefer transform/opacity for animations */
transform: translateX(100px);
opacity: 0.8;
/* Avoid layout-triggering properties */
/* width: 100px; ❌ Triggers layout */
/* height: 50px; ❌ Triggers layout */
}
/* Use CSS containment for performance */
.card-container {
contain: layout style paint;
}
Image Optimization
Responsive Images
<!-- Use srcset for different screen densities -->
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 600px) 400px,
(max-width: 900px) 800px,
1200px"
alt="App hero image"
loading="lazy"
/>
<!-- Modern format support with fallbacks -->
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img src="hero.jpg" alt="App hero image" />
</picture>
Image Loading Strategy
// Progressive image loading component
function OptimizedImage({ src, alt, className, priority = false }) {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
return (
<div className={`image-container ${className}`}>
{!loaded && !error && <ImageSkeleton />}
<img
src={src}
alt={alt}
loading={priority ? "eager" : "lazy"}
onLoad={() => setLoaded(true)}
onError={() => setError(true)}
style={{
opacity: loaded ? 1 : 0,
transition: "opacity 0.3s ease",
}}
/>
{error && <ImageErrorState />}
</div>
);
}
Network Optimization
Resource Hints
<!-- Preload critical resources -->
<link
rel="preload"
href="/fonts/inter.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="preload" href="/api/initial-data" as="fetch" crossorigin />
<!-- Prefetch likely next resources -->
<link rel="prefetch" href="/game/assets.js" />
<link rel="prefetch" href="/profile/components.js" />
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://api.base.org" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
Caching Strategy
// Service Worker caching example
const CACHE_NAME = "miniapp-v1";
const STATIC_ASSETS = ["/", "/app.js", "/app.css", "/manifest.json"];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener("fetch", (event) => {
if (event.request.destination === "image") {
// Cache images with stale-while-revalidate
event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
// Serve from cache, update in background
fetch(event.request).then((fetchResponse) => {
const responseClone = fetchResponse.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
});
return response;
}
return fetch(event.request);
})
);
}
});
Testing & Measurement
Lighthouse Testing
Use Lighthouse to validate performance:
# Test with mobile settings
lighthouse --preset=mobile --output=html --output-path=./report.html https://your-app.com
# Test specific performance budget
lighthouse --budget-path=./budget.json https://your-app.com
Budget Configuration
// budget.json
[
{
"resourceSizes": [
{
"resourceType": "script",
"budget": 200
},
{
"resourceType": "stylesheet",
"budget": 50
},
{
"resourceType": "image",
"budget": 300
}
],
"timings": [
{
"metric": "first-contentful-paint",
"budget": 1200
},
{
"metric": "largest-contentful-paint",
"budget": 2500
}
]
}
]
// Web Vitals monitoring
import { getCLS, getFID, getFCP, getLCP, getTTFB } from "web-vitals";
function sendToAnalytics(metric) {
// Send performance data to your analytics service
analytics.track("Web Vital", {
name: metric.name,
value: metric.value,
id: metric.id,
rating: metric.rating,
});
}
// Measure all Web Vitals
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
// Custom performance tracking
function trackCustomMetric(name, value) {
performance.mark(`${name}-start`);
// ... your code ...
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const measure = performance.getEntriesByName(name)[0];
sendToAnalytics({
name,
value: measure.duration,
rating: measure.duration < 100 ? "good" : "needs-improvement",
});
}
Real User Monitoring
// Performance observer for real user metrics
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === "navigation") {
sendToAnalytics({
name: "page-load-time",
value: entry.loadEventEnd - entry.fetchStart,
});
}
if (entry.entryType === "resource") {
if (entry.transferSize > 1000000) {
// > 1MB
console.warn(
"Large resource detected:",
entry.name,
entry.transferSize
);
}
}
}
});
observer.observe({ entryTypes: ["navigation", "resource"] });
❌ JavaScript Problems
Bundle Bloat
// Bad: Importing entire library
import _ from "lodash"; // Imports all of lodash
// Good: Import only what you need
import { debounce, throttle } from "lodash-es";
// Bad: Large polyfills for all browsers
import "core-js/stable";
// Good: Targeted polyfills
import "core-js/features/array/flat";
Inefficient Rendering
// Bad: Expensive operations in render
function ExpensiveComponent({ data }) {
const processedData = data.map((item) => {
// Heavy computation on every render
return expensiveCalculation(item);
});
return (
<div>
{processedData.map((item) => (
<Item key={item.id} {...item} />
))}
</div>
);
}
// Good: Memoize expensive calculations
function OptimizedComponent({ data }) {
const processedData = useMemo(() => {
return data.map((item) => expensiveCalculation(item));
}, [data]);
return (
<div>
{processedData.map((item) => (
<Item key={item.id} {...item} />
))}
</div>
);
}
❌ CSS Problems
Inefficient Selectors
/* Bad: Complex selectors */
.app .main .content .card .header .title {
}
/* Good: Simple class-based selectors */
.card-title {
}
/* Bad: Universal selectors */
* {
box-sizing: border-box;
}
/* Good: Scoped resets */
.app *,
.app *::before,
.app *::after {
box-sizing: border-box;
}
Layout Thrashing
/* Bad: Properties that trigger layout */
.animation {
transition: width 0.3s, height 0.3s, top 0.3s;
}
/* Good: Transform and opacity only */
.animation {
transition: transform 0.3s, opacity 0.3s;
}
❌ Image Problems
Unoptimized Images
<!-- Bad: Single large image for all devices -->
<img src="hero-2000x1000.jpg" width="100%" />
<!-- Good: Responsive images -->
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
/>
Build Optimization
Runtime Optimization
Network Optimization
Measurement
Resources
Continuous MonitoringPerformance is not a one-time optimization. Continuously monitor your app’s performance and address regressions quickly to maintain featuring eligibility.