{"id":321,"date":"2026-09-07T13:22:09","date_gmt":"2026-09-04T13:22:09","guid":{"rendered":"https:\/\/softcrony.com\/blog\/?p=321"},"modified":"2026-09-04T13:22:09","modified_gmt":"2026-09-04T13:22:09","slug":"performance-audit-ai-built-applications","status":"publish","type":"post","link":"https:\/\/softcrony.com\/blog\/performance-audit-ai-built-applications\/","title":{"rendered":"Performance Audit for AI-Built Apps: Finding the Hidden Bottlenecks"},"content":{"rendered":"<p>This is Part 5 of our <a href=\"https:\/\/softcrony.com\/blog\/ai-agentic-editor-code-audit-guide-developers\/\">AI Code Audit Series<\/a> \u2014 covering the essential audits every team should run when building with AI agentic editors.<\/p>\n<p>Performance problems in AI-generated code share one characteristic that makes them particularly dangerous: they&#8217;re invisible in development. The application works correctly on a developer&#8217;s machine with a few hundred test records. Tests pass. The feature ships. Three months later, with real data volumes and real concurrent users, response times climb, queries time out, and users complain that the application is slow.<\/p>\n<p>By that point, the performance problems are embedded across the codebase \u2014 in query patterns, missing indexes, absent caching, and architectural decisions that made sense for development-scale data but fail at production scale. Fixing them after the fact is significantly more expensive than finding them before launch.<\/p>\n<p>This post covers the specific performance failure patterns AI agentic editors introduce consistently, how to find them before they affect real users, and how to fix them correctly.<\/p>\n<h2>Why AI-Generated Code Has Performance Problems<\/h2>\n<p>AI coding tools optimise for correctness on the inputs described in the prompt \u2014 not for performance at scale. When you ask an AI to implement a feature, it generates code that works correctly for the use case described. It doesn&#8217;t consider what happens when the relevant database table has two million rows instead of fifty test records, or when five hundred users hit the same endpoint simultaneously instead of one developer testing locally.<\/p>\n<p>The AI also doesn&#8217;t know your data volumes, your expected concurrent users, your response time requirements, or your infrastructure constraints. It makes reasonable default choices for a generic application \u2014 choices that are adequate at small scale and increasingly problematic as the application grows.<\/p>\n<p>Performance problems are also invisible to functional testing. An N+1 query that makes 200 database calls instead of 2 returns the correct data \u2014 it just takes forty times longer. A missing database index returns correct results \u2014 queries just scan the full table instead of using the index. These problems pass all functional tests and appear only under load profiling or in production monitoring.<\/p>\n<h2>Performance Problem 1 \u2014 N+1 Query Problems<\/h2>\n<p>This is the most common and most impactful performance problem in AI-generated Laravel code. The AI generates code that loads a collection of records and then accesses a relationship on each one inside a loop \u2014 producing one query to load the collection and one additional query per record to load the relationship.<\/p>\n<pre><code>\/\/ VULNERABLE \u2014 N+1 query problem\r\n\/\/ AI-generated endpoint \u2014 looks correct, performs terribly at scale\r\npublic function index(): JsonResponse\r\n{\r\n    $orders = Order::where('status', 'pending')->get();\r\n    \/\/ One query: SELECT * FROM orders WHERE status = 'pending'\r\n    \/\/ Returns 500 orders\r\n\r\n    return OrderResource::collection($orders);\r\n}\r\n\r\n\/\/ In OrderResource.php \u2014 AI-generated\r\npublic function toArray(Request $request): array\r\n{\r\n    return [\r\n        'id'           => $this->id,\r\n        'total'        => $this->total,\r\n        'customer'     => $this->user->name,        \/\/ Query 2, 3, 4... 501\r\n        'items_count'  => $this->items->count(),    \/\/ Query 502, 503... 1001\r\n        'latest_item'  => $this->items->first()->name, \/\/ Using already-loaded collection\r\n        'address'      => $this->shippingAddress->full_address, \/\/ Query 1002... 1501\r\n    ];\r\n}\r\n\/\/ Total: 1 + 500 + 500 + 500 = 1,501 queries for one page request\r\n\r\n\/\/ CORRECT \u2014 eager loading eliminates N+1\r\npublic function index(): JsonResponse\r\n{\r\n    $orders = Order::where('status', 'pending')\r\n        ->with([\r\n            'user:id,name,email',           \/\/ Only load needed columns\r\n            'items',                         \/\/ Load all items in one query\r\n            'shippingAddress:id,order_id,full_address',\r\n        ])\r\n        ->get();\r\n    \/\/ Total: 4 queries regardless of how many orders exist\r\n\r\n    return OrderResource::collection($orders);\r\n}\r\n\r\n\/\/ Even better \u2014 paginate to limit result set\r\npublic function index(): JsonResponse\r\n{\r\n    $orders = Order::where('status', 'pending')\r\n        ->with(['user:id,name', 'items', 'shippingAddress:id,order_id,full_address'])\r\n        ->latest()\r\n        ->paginate(25); \/\/ Never load unbounded result sets\r\n\r\n    return OrderResource::collection($orders);\r\n}<\/code><\/pre>\n<p>N+1 problems compound with result set size. On a development database with 50 orders, the difference between 51 queries and 4 queries is imperceptible. On a production database with 10,000 orders, the difference is the application being functional versus completely unusable.<\/p>\n<p><strong>Finding N+1 problems \u2014 use Laravel Debugbar or Telescope:<\/strong><\/p>\n<pre><code># Install Laravel Debugbar for development\r\ncomposer require --dev barryvdh\/laravel-debugbar\r\n\r\n# Install Laravel Telescope for development\/staging\r\ncomposer require --dev laravel\/telescope\r\nphp artisan telescope:install\r\nphp artisan migrate\r\n\r\n# Telescope shows all queries per request \u2014 look for:\r\n# - Same query repeated multiple times with different ID values\r\n# - Query count that scales with result set size\r\n\r\n# Automated detection with Laravel's query log\r\nDB::enableQueryLog();\r\n\r\n$orders = Order::where('status', 'pending')->get();\r\n$resource = OrderResource::collection($orders);\r\n\r\n$queries = DB::getQueryLog();\r\n$duplicatePatterns = collect($queries)\r\n    ->groupBy(fn($q) => preg_replace('\/\\d+\/', '?', $q['query']))\r\n    ->filter(fn($group) => $group->count() > 5); \/\/ Flag query patterns repeating >5 times\r\n\r\nLog::warning('Potential N+1 detected', ['patterns' => $duplicatePatterns->keys()]);<\/code><\/pre>\n<h2>Performance Problem 2 \u2014 Missing Database Indexes<\/h2>\n<p>AI-generated migrations create tables with primary keys but frequently miss indexes on columns used in WHERE clauses, ORDER BY clauses, and foreign keys. On small datasets this is imperceptible. On production datasets it means full table scans for every query.<\/p>\n<pre><code>\/\/ AI-generated migration \u2014 missing critical indexes\r\nSchema::create('orders', function (Blueprint $table) {\r\n    $table->id();\r\n    $table->foreignId('user_id')->constrained();\r\n    $table->string('status');           \/\/ Frequently filtered \u2014 no index\r\n    $table->string('order_number');     \/\/ Frequently searched \u2014 no index  \r\n    $table->timestamp('created_at');    \/\/ Frequently sorted \u2014 no index\r\n    $table->decimal('total', 10, 2);\r\n    $table->timestamps();\r\n});\r\n\r\n\/\/ CORRECT \u2014 indexes on every column used in WHERE, ORDER BY, GROUP BY\r\nSchema::create('orders', function (Blueprint $table) {\r\n    $table->id();\r\n    $table->foreignId('user_id')->constrained()->index(); \/\/ Foreign key needs index\r\n    $table->string('status', 20)->index();                \/\/ Filtered frequently\r\n    $table->string('order_number', 50)->unique();         \/\/ Searched and must be unique\r\n    $table->decimal('total', 10, 2);\r\n    $table->timestamps();\r\n\r\n    \/\/ Composite index for common query pattern: user's orders by status\r\n    $table->index(['user_id', 'status']);\r\n\r\n    \/\/ Composite index for admin dashboard: orders by status, newest first\r\n    $table->index(['status', 'created_at']);\r\n});\r\n\r\n\/\/ Add missing indexes to existing tables via new migration\r\nSchema::table('orders', function (Blueprint $table) {\r\n    $table->index('status');\r\n    $table->index(['user_id', 'status']);\r\n    $table->index(['status', 'created_at']);\r\n});<\/code><\/pre>\n<p><strong>Finding missing indexes \u2014 query EXPLAIN output:<\/strong><\/p>\n<pre><code># Check query execution plan for slow queries\r\n# Run in MySQL\/MariaDB directly\r\nEXPLAIN SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC;\r\n# Look for: type = 'ALL' (full table scan \u2014 needs index)\r\n# Good results: type = 'ref' or 'range' (using index)\r\n\r\n# Find queries doing full table scans in Laravel\r\nDB::listen(function ($query) {\r\n    if ($query->time > 100) { \/\/ Queries taking more than 100ms\r\n        $explain = DB::select('EXPLAIN ' . $query->sql, $query->bindings);\r\n        $fullScans = collect($explain)->filter(fn($row) => $row->type === 'ALL');\r\n        \r\n        if ($fullScans->isNotEmpty()) {\r\n            Log::warning('Full table scan detected', [\r\n                'sql'  => $query->sql,\r\n                'time' => $query->time,\r\n            ]);\r\n        }\r\n    }\r\n});\r\n\r\n# Find foreign keys without indexes\r\nSELECT \r\n    TABLE_NAME, COLUMN_NAME\r\nFROM \r\n    INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu\r\nWHERE \r\n    kcu.REFERENCED_TABLE_NAME IS NOT NULL\r\n    AND NOT EXISTS (\r\n        SELECT 1 FROM INFORMATION_SCHEMA.STATISTICS s\r\n        WHERE s.TABLE_SCHEMA = kcu.TABLE_SCHEMA\r\n        AND s.TABLE_NAME = kcu.TABLE_NAME\r\n        AND s.COLUMN_NAME = kcu.COLUMN_NAME\r\n    )\r\nAND kcu.TABLE_SCHEMA = 'your_database_name';<\/code><\/pre>\n<h2>Performance Problem 3 \u2014 Missing Caching<\/h2>\n<p>AI-generated code hits the database for every request \u2014 including requests for data that rarely changes. Configuration values, category lists, product catalogues, site settings, and aggregated statistics are all recalculated from scratch on every request when a simple cache would serve the same data in microseconds.<\/p>\n<pre><code>\/\/ PROBLEMATIC \u2014 expensive queries on every request\r\npublic function getCategories(): JsonResponse\r\n{\r\n    \/\/ Called on every page load, result rarely changes\r\n    $categories = Category::with('children')\r\n        ->whereNull('parent_id')\r\n        ->orderBy('sort_order')\r\n        ->get();\r\n\r\n    return response()->json($categories);\r\n}\r\n\r\npublic function getDashboardStats(): JsonResponse\r\n{\r\n    \/\/ Expensive aggregations recalculated on every dashboard load\r\n    return response()->json([\r\n        'total_orders'   => Order::count(),\r\n        'pending_orders' => Order::where('status', 'pending')->count(),\r\n        'total_revenue'  => Order::where('status', 'delivered')->sum('total'),\r\n        'new_customers'  => User::whereDate('created_at', today())->count(),\r\n    ]);\r\n}\r\n\r\n\/\/ CORRECT \u2014 cache results that don't change frequently\r\npublic function getCategories(): JsonResponse\r\n{\r\n    $categories = Cache::remember('categories_tree', now()->addHours(6), function () {\r\n        return Category::with('children')\r\n            ->whereNull('parent_id')\r\n            ->orderBy('sort_order')\r\n            ->get();\r\n    });\r\n\r\n    return response()->json($categories);\r\n}\r\n\r\n\/\/ Clear cache when categories change\r\npublic static function boot(): void\r\n{\r\n    parent::boot();\r\n\r\n    static::saved(fn() => Cache::forget('categories_tree'));\r\n    static::deleted(fn() => Cache::forget('categories_tree'));\r\n}\r\n\r\n\/\/ Dashboard stats \u2014 cache with shorter TTL, refresh in background\r\npublic function getDashboardStats(): JsonResponse\r\n{\r\n    $stats = Cache::remember('dashboard_stats', now()->addMinutes(5), function () {\r\n        return [\r\n            'total_orders'   => Order::count(),\r\n            'pending_orders' => Order::where('status', 'pending')->count(),\r\n            'total_revenue'  => Order::where('status', 'delivered')->sum('total'),\r\n            'new_customers'  => User::whereDate('created_at', today())->count(),\r\n        ];\r\n    });\r\n\r\n    return response()->json($stats);\r\n}\r\n\r\n\/\/ For user-specific data \u2014 cache per user\r\npublic function getUserDashboard(Request $request): JsonResponse\r\n{\r\n    $cacheKey = \"user_dashboard_{$request->user()->id}\";\r\n\r\n    $data = Cache::remember($cacheKey, now()->addMinutes(15), function () use ($request) {\r\n        return [\r\n            'orders'          => $request->user()->orders()->latest()->take(5)->get(),\r\n            'loyalty_points'  => $request->user()->loyalty_points,\r\n            'pending_returns' => $request->user()->returns()->where('status', 'pending')->count(),\r\n        ];\r\n    });\r\n\r\n    return response()->json($data);\r\n}<\/code><\/pre>\n<p><strong>Finding cacheable queries \u2014 identify repeated expensive queries:<\/strong><\/p>\n<pre><code># In Laravel Telescope \u2014 filter by slow queries\r\n# Look for:\r\n# 1. Same query appearing in multiple requests with identical results\r\n# 2. Aggregation queries (COUNT, SUM, AVG) running on large tables\r\n# 3. Queries with many JOINs running on every request\r\n\r\n# Profile with clock\r\n$start = microtime(true);\r\n$categories = Category::with('children')->whereNull('parent_id')->get();\r\n$elapsed = (microtime(true) - $start) * 1000;\r\n\r\n\/\/ Any query consistently taking >50ms on a warm database is a cache candidate<\/code><\/pre>\n<h2>Performance Problem 4 \u2014 Inefficient Query Patterns<\/h2>\n<p>Beyond N+1 problems, AI-generated code produces several other inefficient query patterns \u2014 loading full records when only a few columns are needed, using collections for operations that should be done in the database, and making multiple queries that could be combined into one.<\/p>\n<pre><code>\/\/ PROBLEMATIC \u2014 inefficient query patterns\r\n\/\/ Loading entire records when only one column is needed\r\npublic function getUserEmails(): array\r\n{\r\n    return User::all()->pluck('email')->toArray();\r\n    \/\/ Loads ALL columns for ALL users into memory, then discards everything except email\r\n}\r\n\r\n\/\/ Doing in PHP what should be done in SQL\r\npublic function getTopProducts(): Collection\r\n{\r\n    $orders = Order::with('items')->get(); \/\/ Loads all orders into memory\r\n    return $orders->flatMap->items        \/\/ Then processes in PHP\r\n        ->groupBy('product_id')\r\n        ->sortByDesc(fn($items) => $items->sum('quantity'))\r\n        ->take(10);\r\n}\r\n\r\n\/\/ Multiple queries that should be one\r\npublic function getOrderSummary(int $orderId): array\r\n{\r\n    $order      = Order::find($orderId);\r\n    $itemCount  = OrderItem::where('order_id', $orderId)->count();\r\n    $total      = OrderItem::where('order_id', $orderId)->sum('subtotal');\r\n\r\n    return ['order' => $order, 'item_count' => $itemCount, 'total' => $total];\r\n}\r\n\r\n\/\/ CORRECT \u2014 efficient equivalents\r\n\/\/ Select only needed columns\r\npublic function getUserEmails(): array\r\n{\r\n    return User::pluck('email')->toArray();\r\n    \/\/ SELECT email FROM users \u2014 minimal data transfer\r\n}\r\n\r\n\/\/ Aggregate in the database, not in PHP\r\npublic function getTopProducts(): Collection\r\n{\r\n    return OrderItem::select('product_id', DB::raw('SUM(quantity) as total_sold'))\r\n        ->groupBy('product_id')\r\n        ->orderByDesc('total_sold')\r\n        ->with('product:id,name,price')\r\n        ->limit(10)\r\n        ->get();\r\n    \/\/ One query, aggregation done by MySQL\r\n}\r\n\r\n\/\/ Single query with eager loaded aggregates\r\npublic function getOrderSummary(int $orderId): array\r\n{\r\n    $order = Order::withCount('items')\r\n        ->withSum('items', 'subtotal')\r\n        ->findOrFail($orderId);\r\n\r\n    return [\r\n        'order'      => $order,\r\n        'item_count' => $order->items_count,\r\n        'total'      => $order->items_sum_subtotal,\r\n    ];\r\n    \/\/ One query with computed aggregates\r\n}<\/code><\/pre>\n<h2>Performance Problem 5 \u2014 Synchronous Processing of Slow Operations<\/h2>\n<p>AI-generated code performs slow operations synchronously \u2014 sending emails, generating PDFs, calling external APIs, processing images \u2014 directly in the request\/response cycle. The user waits for the operation to complete before receiving a response.<\/p>\n<pre><code>\/\/ PROBLEMATIC \u2014 slow operations blocking the response\r\npublic function submitApplication(Request $request): JsonResponse\r\n{\r\n    $application = Application::create($request->validated());\r\n\r\n    \/\/ These three operations block the response \u2014 could take 5-15 seconds total\r\n    Mail::to($request->user())->send(new ApplicationConfirmationMail($application));\r\n    Mail::to(config('mail.admin'))->send(new NewApplicationNotificationMail($application));\r\n    $this->pdfService->generateApplicationPdf($application); \/\/ Slow\r\n    $this->crmService->createLead($application);             \/\/ External API call\r\n\r\n    return response()->json(['message' => 'Application submitted'], 201);\r\n    \/\/ User waits 5-15 seconds for this response\r\n}\r\n\r\n\/\/ CORRECT \u2014 queue slow operations, respond immediately\r\npublic function submitApplication(Request $request): JsonResponse\r\n{\r\n    $application = Application::create($request->validated());\r\n\r\n    \/\/ Dispatch to queue \u2014 returns immediately\r\n    SendApplicationConfirmation::dispatch($application);\r\n    NotifyAdminOfApplication::dispatch($application);\r\n    GenerateApplicationPdf::dispatch($application);\r\n    SyncApplicationToCrm::dispatch($application);\r\n\r\n    return response()->json(['message' => 'Application submitted'], 201);\r\n    \/\/ Response returns in <200ms \u2014 queue workers handle the rest\r\n}\r\n\r\n\/\/ app\/Jobs\/SendApplicationConfirmation.php\r\nclass SendApplicationConfirmation implements ShouldQueue\r\n{\r\n    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\r\n\r\n    public int $tries = 3;\r\n    public int $backoff = 60; \/\/ Retry after 60 seconds on failure\r\n\r\n    public function __construct(private Application $application) {}\r\n\r\n    public function handle(): void\r\n    {\r\n        Mail::to($this->application->user)\r\n            ->send(new ApplicationConfirmationMail($this->application));\r\n    }\r\n\r\n    public function failed(\\Throwable $exception): void\r\n    {\r\n        Log::error('Failed to send application confirmation', [\r\n            'application_id' => $this->application->id,\r\n            'error'          => $exception->getMessage(),\r\n        ]);\r\n    }\r\n}<\/code><\/pre>\n<p><strong>Finding synchronous slow operations:<\/strong><\/p>\n<pre><code># Find Mail::send() calls in controllers \u2014 should be queued\r\ngrep -rn \"Mail::send\\|Mail::to\" app\/Http\/Controllers --include=\"*.php\"\r\n\r\n# Find external HTTP calls in controllers \u2014 should be queued\r\ngrep -rn \"Http::get\\|Http::post\\|Guzzle\\|curl_exec\" app\/Http\/Controllers --include=\"*.php\"\r\n\r\n# Find PDF\/image generation in controllers\r\ngrep -rn \"PDF::\\|Imagick\\|GdImage\\|Intervention\" app\/Http\/Controllers --include=\"*.php\"\r\n\r\n# All of the above should be in Jobs, not Controllers<\/code><\/pre>\n<h2>Performance Problem 6 \u2014 Unbounded Result Sets<\/h2>\n<p>AI-generated API endpoints frequently return all matching records without pagination \u2014 loading potentially thousands of records into memory, serialising them all, and sending them all to the client.<\/p>\n<pre><code>\/\/ PROBLEMATIC \u2014 unbounded query\r\npublic function index(): JsonResponse\r\n{\r\n    $products = Product::where('active', true)->get(); \/\/ Could be 50,000 records\r\n    return ProductResource::collection($products);\r\n    \/\/ Memory: potentially hundreds of MB\r\n    \/\/ Response time: unpredictable\r\n    \/\/ JSON response size: potentially tens of MB\r\n}\r\n\r\n\/\/ CORRECT \u2014 always paginate collection endpoints\r\npublic function index(Request $request): JsonResponse\r\n{\r\n    $products = Product::where('active', true)\r\n        ->when($request->category_id, fn($q) => $q->where('category_id', $request->category_id))\r\n        ->when($request->search, fn($q) => $q->where('name', 'LIKE', \"%{$request->search}%\"))\r\n        ->orderBy('name')\r\n        ->paginate($request->integer('per_page', 24));\r\n        \/\/ Maximum 24 records per request regardless of total count\r\n\r\n    return ProductResource::collection($products);\r\n    \/\/ Includes pagination metadata: current_page, last_page, total, per_page\r\n}\r\n\r\n\/\/ For internal processing \u2014 chunk large datasets\r\npublic function exportAllOrders(): void\r\n{\r\n    \/\/ Never: Order::all()->each(fn($order) => $this->process($order));\r\n\r\n    Order::chunk(500, function ($orders) {\r\n        foreach ($orders as $order) {\r\n            $this->process($order);\r\n        }\r\n        \/\/ 500 records loaded, processed, released from memory\r\n        \/\/ Then next 500 loaded \u2014 constant memory usage regardless of total\r\n    });\r\n\r\n    \/\/ Or use lazy loading for read-only processing\r\n    Order::lazy()->each(fn($order) => $this->process($order));\r\n}<\/code><\/pre>\n<p><strong>Finding unbounded queries:<\/strong><\/p>\n<pre><code># Find ->get() calls without preceding ->paginate() or ->limit()\r\ngrep -rn \"->get()\" app\/Http\/Controllers --include=\"*.php\" | \\\r\n  grep -v \"->limit\\|->first\\|->find\\|paginate\"\r\n\r\n# Find collection endpoints returning without pagination metadata\r\ngrep -rn \"Resource::collection\" app\/Http\/Controllers --include=\"*.php\" -B5 | \\\r\n  grep -v \"paginate\\|->limit\"<\/code><\/pre>\n<h2>The Performance Audit Process<\/h2>\n<p><strong>Step 1 \u2014 Profile before optimising.<\/strong> Never optimise based on intuition. Use Laravel Telescope, Laravel Debugbar, or Clockwork to get actual data on which queries are slowest, which endpoints have the highest query counts, and where time is actually being spent. Optimising the wrong thing wastes effort and sometimes makes performance worse.<\/p>\n<pre><code># Install Clockwork for detailed profiling\r\ncomposer require --dev itsgoingd\/clockwork\r\n\r\n# Enable query logging in AppServiceProvider for development\r\nDB::listen(function ($query) {\r\n    if ($query->time > 50) {\r\n        Log::channel('slow_queries')->warning('Slow query detected', [\r\n            'sql'      => $query->sql,\r\n            'bindings' => $query->bindings,\r\n            'time'     => $query->time . 'ms',\r\n        ]);\r\n    }\r\n});<\/code><\/pre>\n<p><strong>Step 2 \u2014 Load test with realistic data volumes.<\/strong> AI-generated code is tested on development datasets. Performance problems are often invisible until data volumes match production. Before launching, seed your staging database with realistic data volumes and run load tests.<\/p>\n<pre><code># Install k6 for load testing\r\n# brew install k6 (Mac) or download from k6.io\r\n\r\n# Basic load test script\r\nimport http from 'k6\/http';\r\nimport { check, sleep } from 'k6';\r\n\r\nexport const options = {\r\n    vus: 50,        \/\/ 50 concurrent users\r\n    duration: '2m', \/\/ Run for 2 minutes\r\n};\r\n\r\nexport default function () {\r\n    const response = http.get('https:\/\/staging.yourapp.com\/api\/products');\r\n    \r\n    check(response, {\r\n        'status is 200':          (r) => r.status === 200,\r\n        'response time < 500ms':  (r) => r.timings.duration < 500,\r\n        'response time < 1000ms': (r) => r.timings.duration < 1000,\r\n    });\r\n    \r\n    sleep(1);\r\n}\r\n\r\n# Run: k6 run load-test.js<\/code><\/pre>\n<p><strong>Step 3 \u2014 Fix in priority order.<\/strong> Address performance issues by impact: N+1 queries first (highest impact per fix), missing indexes second (one migration affects many queries), unbounded queries third (pagination protects all result sets), then caching and async processing.<\/p>\n<h2>The Performance Audit Checklist<\/h2>\n<p><strong>Query Efficiency<\/strong><\/p>\n<ul>\n<li>All relationship access in loops uses eager loading with with()<\/li>\n<li>Only required columns are selected \u2014 no SELECT * on large tables<\/li>\n<li>Aggregations (COUNT, SUM, AVG) are done in SQL not PHP collections<\/li>\n<li>No queries inside loops \u2014 all batch operations use collections or chunking<\/li>\n<\/ul>\n<p><strong>Database Indexes<\/strong><\/p>\n<ul>\n<li>All foreign key columns have indexes<\/li>\n<li>All columns used in WHERE clauses have indexes<\/li>\n<li>All columns used in ORDER BY clauses have indexes<\/li>\n<li>Composite indexes exist for common multi-column query patterns<\/li>\n<li>EXPLAIN output shows no full table scans on production-scale data<\/li>\n<\/ul>\n<p><strong>Caching<\/strong><\/p>\n<ul>\n<li>Configuration and reference data (categories, settings) is cached<\/li>\n<li>Expensive aggregations are cached with appropriate TTL<\/li>\n<li>Cache is invalidated correctly when underlying data changes<\/li>\n<li>Cache keys are namespaced to avoid collisions<\/li>\n<\/ul>\n<p><strong>Async Processing<\/strong><\/p>\n<ul>\n<li>Email sending uses queued jobs \u2014 no Mail::send() in controllers<\/li>\n<li>External API calls use queued jobs<\/li>\n<li>File generation (PDF, CSV export) uses queued jobs<\/li>\n<li>Image processing uses queued jobs<\/li>\n<li>Queue workers are configured and monitored in production<\/li>\n<\/ul>\n<p><strong>Result Set Management<\/strong><\/p>\n<ul>\n<li>All collection API endpoints use pagination<\/li>\n<li>Large dataset processing uses chunk() or lazy()<\/li>\n<li>Per-page limits are enforced \u2014 user cannot request unlimited records<\/li>\n<li>Search results have reasonable maximum limits<\/li>\n<\/ul>\n<p>Next in the series: <a href=\"https:\/\/softcrony.com\/blog\/database-audit-ai-generated-schema-queries\/\">Database Audit<\/a> \u2014 reviewing AI-generated schemas, migrations, and queries for correctness, safety, and long-term maintainability.<\/p>\n<p>If you want a performance audit run on your existing application \u2014 profiling, identifying bottlenecks, and a prioritised optimisation plan \u2014 <a href=\"https:\/\/softcrony.com\/contact\/\">our team at Softcrony is happy to help<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is Part 5 of our AI Code Audit Series \u2014 covering the essential audits every team should run when building with AI agentic editors. Performance problems in AI-generated code share one characteristic that makes them particularly dangerous: they&#8217;re invisible in development. The application works correctly on a developer&#8217;s machine with a few hundred test [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":322,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[208,236,237,53,234,235,233,238,239],"class_list":["post-321","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","tag-ai-coding","tag-caching","tag-database-optimization","tag-devops","tag-laravel-performance","tag-n1-query","tag-performance-audit","tag-php-performance","tag-query-optimization"],"_links":{"self":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/321","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/comments?post=321"}],"version-history":[{"count":1,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions"}],"predecessor-version":[{"id":323,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/posts\/321\/revisions\/323"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media\/322"}],"wp:attachment":[{"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/media?parent=321"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/categories?post=321"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softcrony.com\/blog\/wp-json\/wp\/v2\/tags?post=321"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}