npx claudepluginhub bramato/laravel-react-plugins --plugin laravel-reactWant just this skill?
Then install: npx claudepluginhub u/[userId]/[slug]
Use when optimizing application performance across Laravel and React. Covers query optimization, caching strategies, eager loading, React rendering optimization, bundle size reduction, and Inertia-specific performance patterns.
This skill uses the workspace's default tool permissions.
Performance Optimization — Laravel + Inertia + React
Comprehensive performance optimization patterns across the full stack: query optimization, caching, React rendering, Inertia-specific patterns, Vite build optimization, and monitoring tools.
1. Query Optimization
N+1 Prevention with Eager Loading
The most common performance issue. Always eager load relationships you will access.
// BAD: N+1 query — 1 query for orders + N queries for each order's user
$orders = Order::all();
foreach ($orders as $order) {
echo $order->user->name; // Triggers a query per iteration
}
// GOOD: Eager loading — 2 queries total
$orders = Order::with('user')->get();
// Multiple relationships
$orders = Order::with(['user', 'items', 'items.product', 'tags'])->get();
// Eager loading with constraints
$orders = Order::with([
'items' => fn ($query) => $query->orderBy('price', 'desc'),
'user:id,name,email', // Select specific columns
])->get();
preventLazyLoading() in AppServiceProvider
Catch N+1 queries during development by throwing an exception whenever a relationship is lazy loaded:
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
// Only in non-production environments
Model::preventLazyLoading(! $this->app->isProduction());
// Optionally log instead of throwing in production
Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) {
logger()->warning("Lazy loading detected: {$model::class}::{$relation}");
});
}
Select Specific Columns
Avoid loading unnecessary data from the database:
// BAD: Loads all columns
$users = User::all();
// GOOD: Select only needed columns
$users = User::select(['id', 'name', 'email'])->get();
// With relationships — always include foreign keys
$orders = Order::select(['id', 'user_id', 'total', 'status', 'created_at'])
->with('user:id,name')
->get();
Chunking for Large Datasets
Process large record sets without exceeding memory limits:
// Process in chunks of 200 records
Order::where('status', 'pending')
->chunk(200, function ($orders) {
foreach ($orders as $order) {
$this->processOrder($order);
}
});
// Lazy collection — memory efficient iteration
Order::where('status', 'pending')
->lazy()
->each(function (Order $order) {
$this->processOrder($order);
});
// chunkById for updates (avoids offset issues)
Order::where('status', 'pending')
->chunkById(200, function ($orders) {
$orders->each->markAsProcessing();
});
Efficient Aggregations
// BAD: Loads all records into memory to count
$count = Order::all()->count();
// GOOD: Database-level aggregation
$count = Order::count();
$total = Order::sum('total');
$avg = Order::where('status', 'delivered')->avg('total');
// Subquery selects for computed values
$users = User::withCount('orders')
->withSum('orders', 'total')
->withAvg('orders', 'total')
->get();
// Access: $user->orders_count, $user->orders_sum_total, $user->orders_avg_total
Exists Instead of Count
// BAD: Counts all records when you only need to know if any exist
if (Order::where('user_id', $userId)->count() > 0) { /* ... */ }
// GOOD: Stops at first match
if (Order::where('user_id', $userId)->exists()) { /* ... */ }
2. Database Indexing Strategies
Migration Index Patterns
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->index(); // Foreign key index
$table->string('status')->index(); // Frequently filtered
$table->decimal('total', 10, 2);
$table->timestamp('shipped_at')->nullable()->index(); // Filtered + sorted
$table->timestamps();
// Composite index for common queries
$table->index(['user_id', 'status']); // WHERE user_id = ? AND status = ?
$table->index(['status', 'created_at']); // WHERE status = ? ORDER BY created_at
$table->index(['user_id', 'created_at']); // User orders sorted by date
});
Index Guidelines
| Scenario | Index Strategy |
|---|---|
| Foreign keys | Always index (Laravel does this with constrained()) |
| WHERE clauses | Index columns used in filters |
| ORDER BY | Include in index after WHERE columns |
| Composite queries | Create composite index matching query column order |
| Low cardinality | Avoid indexing boolean/enum columns alone |
| Full-text search | Use fullText() index or dedicated search (Meilisearch) |
Identifying Missing Indexes
// Use EXPLAIN to analyze slow queries
DB::listen(function ($query) {
if ($query->time > 100) { // Log queries over 100ms
logger()->warning('Slow query detected', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time_ms' => $query->time,
]);
}
});
3. Laravel Caching
Config, Route, and View Caching
# Production optimization commands
php artisan config:cache # Cache config files into single file
php artisan route:cache # Cache route registrations
php artisan view:cache # Pre-compile Blade views
php artisan event:cache # Cache event-listener mappings
php artisan icons:cache # Cache Blade icons (if using blade-icons)
# Clear caches
php artisan optimize:clear # Clears all caches at once
Query Result Caching
use Illuminate\Support\Facades\Cache;
class ProductService
{
public function getFeaturedProducts(): Collection
{
return Cache::remember('products:featured', now()->addHour(), function () {
return Product::where('is_featured', true)
->with('category')
->orderBy('sort_order')
->get();
});
}
public function getCategories(): Collection
{
return Cache::remember('categories:all', now()->addDay(), function () {
return Category::withCount('products')
->orderBy('name')
->get();
});
}
/**
* Invalidate cache when data changes.
*/
public function updateProduct(Product $product, array $data): Product
{
$product->update($data);
Cache::forget('products:featured');
Cache::forget("products:{$product->id}");
return $product->fresh();
}
}
Tagged Cache (Redis/Memcached Only)
// Group related cache entries with tags for bulk invalidation
Cache::tags(['orders', 'user:1'])->put('user:1:orders', $orders, 3600);
Cache::tags(['orders', 'user:2'])->put('user:2:orders', $orders, 3600);
// Invalidate all order caches at once
Cache::tags('orders')->flush();
// Invalidate all caches for a specific user
Cache::tags('user:1')->flush();
Redis Caching Patterns
// config/database.php — Redis connection
'redis' => [
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
// Usage with Redis-specific features
use Illuminate\Support\Facades\Redis;
// Atomic counter
Redis::incr('orders:count:today');
// Sorted set for leaderboard
Redis::zadd('products:popular', $viewCount, "product:{$id}");
$topProducts = Redis::zrevrange('products:popular', 0, 9);
// Lock for preventing race conditions
$lock = Cache::lock('order:process:' . $orderId, 10);
if ($lock->get()) {
try {
$this->processOrder($order);
} finally {
$lock->release();
}
}
4. Queue Processing for Heavy Tasks
Offload slow operations to background queues to keep response times fast.
// Dispatch expensive operations to the queue
class OrderService
{
public function create(OrderDTO $dto): Order
{
$order = DB::transaction(function () use ($dto) {
return Order::create($dto->toArray());
});
// These run in the background — response returns immediately
GenerateInvoicePdf::dispatch($order);
SendOrderConfirmation::dispatch($order);
SyncOrderToExternalSystem::dispatch($order)->onQueue('integrations');
UpdateAnalytics::dispatch($order)->delay(now()->addMinutes(5));
return $order;
}
}
// Job class
class GenerateInvoicePdf implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public readonly Order $order,
) {}
public function handle(PdfGenerator $pdf): void
{
$pdf->generate($this->order);
}
public int $tries = 3;
public int $backoff = 60; // Wait 60s between retries
}
5. React Performance
React.memo — Prevent Unnecessary Re-renders
import { memo } from 'react';
// Without memo: re-renders every time parent re-renders
// With memo: only re-renders when props change
const OrderRow = memo(function OrderRow({ order }: { order: Order }) {
return (
<tr>
<td>{order.customer_name}</td>
<td>{order.formatted_total}</td>
<td>{order.status}</td>
</tr>
);
});
// Custom comparison function for complex props
const OrderCard = memo(
function OrderCard({ order, onEdit }: Props) {
return (/* ... */);
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.order.id === nextProps.order.id
&& prevProps.order.updated_at === nextProps.order.updated_at;
}
);
useMemo — Cache Expensive Computations
import { useMemo } from 'react';
function OrderDashboard({ orders }: { orders: Order[] }) {
// Recomputes only when orders array changes
const stats = useMemo(() => ({
totalRevenue: orders.reduce((sum, o) => sum + o.total, 0),
averageOrder: orders.length > 0
? orders.reduce((sum, o) => sum + o.total, 0) / orders.length
: 0,
byStatus: orders.reduce((acc, o) => {
acc[o.status] = (acc[o.status] || 0) + 1;
return acc;
}, {} as Record<string, number>),
}), [orders]);
// Filtered list — recomputes only when dependencies change
const pendingOrders = useMemo(
() => orders.filter(o => o.status === 'pending'),
[orders]
);
return (/* render stats and pendingOrders */);
}
useCallback — Stable Function References
import { useCallback, useState } from 'react';
function OrderList({ orders }: { orders: Order[] }) {
const [selectedId, setSelectedId] = useState<number | null>(null);
// Without useCallback: new function on every render,
// causing all child OrderRow components to re-render
const handleSelect = useCallback((id: number) => {
setSelectedId(id);
}, []); // Empty deps: function never changes
const handleDelete = useCallback((id: number) => {
router.delete(route('orders.destroy', id), {
preserveScroll: true,
});
}, []);
return (
<table>
{orders.map(order => (
<OrderRow
key={order.id}
order={order}
isSelected={order.id === selectedId}
onSelect={handleSelect}
onDelete={handleDelete}
/>
))}
</table>
);
}
When to Use Each
| Hook | Use When | Skip When |
|---|---|---|
React.memo | Component receives same props frequently, rendering is expensive | Component always receives new props, or rendering is cheap |
useMemo | Expensive computation, large list filtering/sorting, derived data | Simple values, already fast computations |
useCallback | Passing callbacks to memoized children, stable event handlers | Not passing to memoized children, simple inline handlers |
6. Code Splitting and Lazy Loading
React.lazy with Suspense
import { lazy, Suspense } from 'react';
// Split large components into separate chunks
const AdminDashboard = lazy(() => import('@/Pages/Admin/Dashboard'));
const ReportViewer = lazy(() => import('@/Components/ReportViewer'));
const RichTextEditor = lazy(() => import('@/Components/RichTextEditor'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<AdminDashboard />
</Suspense>
);
}
// Inline fallback for smaller components
function OrderDetail({ order }: Props) {
return (
<div>
<h1>Order #{order.id}</h1>
<Suspense fallback={<div className="h-64 animate-pulse bg-gray-200 rounded" />}>
<ReportViewer orderId={order.id} />
</Suspense>
</div>
);
}
Inertia Page-Level Code Splitting
Inertia automatically supports code splitting when you use dynamic imports in your resolve function:
// resources/js/app.tsx
createInertiaApp({
resolve: (name) => {
// Automatic code splitting — each page becomes its own chunk
const pages = import.meta.glob('./Pages/**/*.tsx');
return pages[`./Pages/${name}.tsx`]();
},
// ...
});
7. Inertia-Specific Optimizations
Partial Reloads
Only request the data you need on subsequent visits:
import { router } from '@inertiajs/react';
// Only reload the 'orders' prop, not the entire page data
function refreshOrders() {
router.reload({ only: ['orders'] });
}
// Use with polling for live updates
useEffect(() => {
const interval = setInterval(() => {
router.reload({ only: ['notifications'] });
}, 30000);
return () => clearInterval(interval);
}, []);
Lazy Props
Props that are expensive to compute can be deferred until explicitly requested:
// Controller — lazy props are not evaluated on initial page load
return Inertia::render('Orders/Show', [
'order' => $order,
'can' => [...],
// Lazy: only loaded when explicitly requested via partial reload
'auditLog' => Inertia::lazy(fn () =>
AuditLog::where('order_id', $order->id)->latest()->limit(50)->get()
),
// Deferred: loaded automatically after initial render
'statistics' => Inertia::defer(fn () =>
$this->statisticsService->forOrder($order)
),
]);
// Frontend: request lazy prop when user clicks
function OrderShow({ order, auditLog, statistics }: Props) {
const loadAuditLog = () => {
router.reload({ only: ['auditLog'] });
};
return (
<div>
<OrderDetails order={order} />
{/* Statistics load automatically after initial render (deferred) */}
{statistics ? <StatsPanel data={statistics} /> : <StatsPlaceholder />}
{/* Audit log loads on demand (lazy) */}
<button onClick={loadAuditLog}>Load Audit Log</button>
{auditLog && <AuditLogTable entries={auditLog} />}
</div>
);
}
Persistent Layouts
Prevent layout components from unmounting between page visits, preserving state (scroll position, audio players, sidebars):
// resources/js/Layouts/AuthenticatedLayout.tsx
export default function AuthenticatedLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen">
<Sidebar />
<main>{children}</main>
</div>
);
}
// resources/js/Pages/Orders/Index.tsx
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout';
function OrderIndex({ orders }: Props) {
return <div>{/* page content */}</div>;
}
// Persistent layout — AuthenticatedLayout is not remounted between pages
OrderIndex.layout = (page: React.ReactNode) => (
<AuthenticatedLayout>{page}</AuthenticatedLayout>
);
export default OrderIndex;
Deferred Props
Load heavy data after the initial page render for faster perceived load times:
return Inertia::render('Dashboard', [
'user' => $user, // Loaded immediately
'recentOrders' => Inertia::defer(fn () => // Loaded after render
Order::forUser($user->id)->latest()->limit(5)->get()
),
'analytics' => Inertia::defer(fn () => // Loaded after render
$this->analyticsService->getDashboardStats()
),
]);
Prefetching Links
import { Link } from '@inertiajs/react';
// Prefetch on hover — starts loading the page data before the user clicks
<Link href={route('orders.show', order.id)} prefetch>
View Order
</Link>
// Prefetch with caching options
<Link href="/dashboard" prefetch cacheFor="1m">
Dashboard
</Link>
8. Vite Build Optimization
Tree Shaking
Ensure unused code is eliminated by using named imports:
// BAD: Imports the entire library
import _ from 'lodash';
_.debounce(fn, 300);
// GOOD: Import only what you need (tree-shakeable)
import { debounce } from 'lodash-es';
debounce(fn, 300);
// GOOD: Even more granular
import debounce from 'lodash/debounce';
Chunk Splitting Configuration
// vite.config.ts
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({ input: 'resources/js/app.tsx', refresh: true }),
react(),
],
build: {
rollupOptions: {
output: {
manualChunks: {
// Separate vendor chunks for better caching
'vendor-react': ['react', 'react-dom'],
'vendor-inertia': ['@inertiajs/react'],
'vendor-ui': ['@headlessui/react', '@heroicons/react'],
},
},
},
chunkSizeWarningLimit: 500, // Warn if chunk exceeds 500kB
},
});
Analyzing Bundle Size
# Install the visualizer plugin
npm install -D rollup-plugin-visualizer
# Add to vite.config.ts and run build
npx vite build
# Opens a visual treemap of your bundle
9. Image Optimization
Responsive Images
function ProductImage({ product }: { product: Product }) {
return (
<img
src={product.image_url}
srcSet={`
${product.image_sm} 400w,
${product.image_md} 800w,
${product.image_lg} 1200w
`}
sizes="(max-width: 640px) 400px, (max-width: 1024px) 800px, 1200px"
alt={product.name}
loading="lazy" // Native lazy loading
decoding="async" // Non-blocking decode
className="w-full h-auto object-cover"
/>
);
}
Laravel Image Processing (Queue)
class ProcessProductImage implements ShouldQueue
{
public function handle(): void
{
$image = Image::make(Storage::get($this->path));
// Generate multiple sizes
foreach ([400, 800, 1200] as $width) {
$resized = $image->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
})->encode('webp', 80);
Storage::put(
"products/{$this->productId}/{$width}.webp",
$resized
);
}
}
}
10. API Response Optimization
Pagination
Always paginate collections. Never return unbounded result sets.
// Standard pagination
$orders = Order::with('user')
->latest()
->paginate(15); // 15 items per page
// Simple pagination (no total count — faster for large tables)
$orders = Order::with('user')
->latest()
->simplePaginate(15); // No "total" count query
// Cursor pagination (most efficient for infinite scroll)
$orders = Order::with('user')
->orderBy('id')
->cursorPaginate(15);
Sparse Fieldsets
Let clients request only the fields they need:
// Controller
public function index(Request $request)
{
$query = Order::query();
// Only load requested fields
if ($fields = $request->input('fields')) {
$columns = array_intersect(
explode(',', $fields),
['id', 'customer_name', 'total', 'status', 'created_at']
);
$query->select(array_merge($columns, ['user_id'])); // Always include FK
}
// Only load requested relationships
if ($request->boolean('include_items')) {
$query->with('items:id,order_id,product_id,quantity,price');
}
return $query->paginate(15);
}
Conditional Relationship Loading
return Inertia::render('Orders/Index', [
'orders' => Order::query()
->with('user:id,name')
->when(request('include') === 'items', fn ($q) => $q->with('items'))
->latest()
->paginate(15),
]);
11. Monitoring and Profiling Tools
Laravel Telescope
Development-level insight into queries, requests, exceptions, and more:
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
// config/telescope.php — only enable locally
'enabled' => env('TELESCOPE_ENABLED', false),
// app/Providers/TelescopeServiceProvider.php
protected function gate(): void
{
Gate::define('viewTelescope', function (User $user) {
return $user->is_admin;
});
}
Access at /telescope to view queries, slow queries, exceptions, mail, notifications, and cache operations.
Laravel Debugbar
In-browser debug toolbar showing queries, route info, and timing:
composer require barryvdh/laravel-debugbar --dev
Automatically displays at the bottom of every page in local development. Shows:
- Number of queries and total query time
- Duplicate queries (N+1 detection)
- Route and middleware information
- Memory usage and execution time
- Cache hits/misses
React DevTools Profiler
Built into React DevTools browser extension:
- Open React DevTools in the browser
- Switch to the Profiler tab
- Click Record and interact with the page
- Review the flamegraph to identify slow renders
- Look for components re-rendering unnecessarily
Key things to look for:
- Components re-rendering when their props have not changed (wrap with
React.memo) - Expensive computations running on every render (wrap with
useMemo) - New function references causing child re-renders (stabilize with
useCallback)
Query Monitoring in Production
// In AppServiceProvider::boot() — production query monitoring
if (! $this->app->isProduction()) {
return;
}
DB::listen(function ($query) {
if ($query->time > 500) { // Log queries over 500ms
Log::channel('slow-queries')->warning('Slow query', [
'sql' => $query->sql,
'bindings' => $query->bindings,
'time_ms' => $query->time,
'url' => request()->fullUrl(),
]);
}
});
Performance Checklist
| Area | Check | Tool |
|---|---|---|
| Queries | No N+1 queries | preventLazyLoading(), Debugbar |
| Queries | Proper eager loading on all controllers | Code review |
| Queries | Indexes on filtered/sorted columns | EXPLAIN, Telescope |
| Caching | Config/route/view cached in production | php artisan optimize |
| Caching | Expensive queries cached with TTL | Cache::remember() |
| Queues | Heavy tasks dispatched to queue | Telescope, Horizon |
| React | No unnecessary re-renders | React DevTools Profiler |
| React | Large components code-split with lazy() | Bundle analyzer |
| Inertia | Partial reloads for polling/filters | Network tab |
| Inertia | Persistent layouts for stable UI | Page transitions |
| Inertia | Lazy/deferred props for heavy data | Controller review |
| Build | Tree shaking enabled, named imports used | Bundle analyzer |
| Build | Vendor chunks split for caching | vite.config.ts |
| Images | Lazy loading, responsive srcSet, WebP | Lighthouse audit |
| API | All collections paginated | Code review |
| Database | Composite indexes for common query patterns | EXPLAIN, slow query log |
Similar Skills
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.