How to define a project scope before the first sprint
A software project scope lists the goal, the users, the flows with acceptance criteria, what is out, and how changes get handled. Our template inside.

Carlos Saldaña
Oct 6, 2026 · 7 min read
To speed up a website, measure real users first, then fix the server response, the database, the main image and the JavaScript, in that order.

Rainer Rodriguez
Co-founder, Nerdify
Sep 15, 2026 · 8 min read
To improve a website's speed, measure what real users experience, find the slowest step, and fix that step before anything else. In the web apps we get asked to speed up, the order is usually the same: the server takes too long to answer, the database does too much work per request, the largest image or block of text loads late, and the browser has to run too much JavaScript before the page responds. Compressing images helps, but it rarely fixes an app that takes two seconds to send its first byte.
This is the process we follow on Laravel and React apps, with the changes that usually make the difference.
Lab tools like Lighthouse run one page load on one simulated device. They are good for catching regressions, and bad at telling you what your users feel. We start with field data: the Core Web Vitals report in Search Console, the "Discover what your real users are experiencing" section of PageSpeed Insights, or the browser's own numbers sent from the app with the web-vitals library.
Google's thresholds for a good experience, measured at the 75th percentile of page loads, are:
| Metric | What it measures | Good |
|---|---|---|
| LCP (Largest Contentful Paint) | When the main content appears | 2.5 s or less |
| INP (Interaction to Next Paint) | How fast the page reacts to clicks and typing | 200 ms or less |
| CLS (Cumulative Layout Shift) | How much the layout jumps while loading | 0.1 or less |
Then we look at Time to First Byte. If the server takes 1.5 seconds to answer, no front-end work will bring LCP under 2.5 seconds on a phone, and that tells us where to start.
For Laravel apps we open the slow page with Laravel Debugbar in a local or staging copy that has realistic data. It shows every query, how long each one took, how many models were hydrated, and the time spent rendering. A page that runs hundreds of queries is a frequent finding, and the cause is usually an N+1, a loop that loads a relation one row at a time.
We make Laravel refuse lazy loading outside production, so new N+1 queries fail in development instead of shipping:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}The fix is eager loading, and it is usually one line:
$orders = Order::query()
->with(['customer', 'items.product'])
->whereBelongsTo($request->user()->team)
->latest()
->paginate(25);On the React side the same problem shows up as waterfalls: a component mounts, fetches, renders a child, and the child fetches again. With Inertia we send the data the page needs as props from one controller, so the browser makes one request instead of five.
Compressing images helps, but it rarely fixes an app that takes two seconds to send its first byte.
After the query count comes query cost. We run EXPLAIN on anything over a few tens of milliseconds and look for full table scans. Most of the time the fix is an index that matches how the query filters and sorts:
return new class extends Migration
{
public function up(): void
{
Schema::table('orders', function (Blueprint $table): void {
$table->index(['team_id', 'status', 'created_at']);
});
}
};Column order in a composite index matters. The columns you filter on with equality go first, and the column you sort or range on goes last. An index on created_at, team_id would not help this query much.
We also check what the query selects. Loading 40 columns, including a large JSON column, to show a name and a date in a list wastes memory on both the database and PHP. select() the columns the page uses.
Dashboards, counts, and reports that aggregate thousands of rows are good cache candidates. We use Cache::flexible, which serves the cached value while it refreshes in the background once it is stale, so no user waits for the recalculation:
use Illuminate\Support\Facades\Cache;
$stats = Cache::flexible("team:{$team->id}:dashboard", [300, 900], function () use ($team): array {
return [
'open_orders' => $team->orders()->where('status', 'open')->count(),
'revenue_cents' => $team->orders()->where('status', 'paid')->sum('total_cents'),
];
});The two numbers mean the value is fresh for five minutes. Between five and fifteen minutes, users get the stale value while it recomputes in the background, and after fifteen minutes it is rebuilt during the request.
Caching hides slow code. We cache only after the query is as fast as it can reasonably be, because a cold cache still has to run it, and the first user after every deploy pays for it.
Sending email, generating PDFs, calling a third-party API and resizing uploads don't need to finish before the user sees a response, so we dispatch a queued job and return.
GenerateInvoicePdf::dispatch($invoice);
return Redirect::route('invoices.show', $invoice);Third-party calls deserve a timeout even when they stay in the request. Laravel's HTTP client waits 30 seconds by default, and one slow vendor can hold a PHP worker for all of it. We set Http::timeout(5) or less, and a retry with backoff where the call is safe to repeat.
Laravel Octane helps when framework boot time is a real share of each request, because the app boots once and serves many requests. It doesn't fix slow queries, and it brings rules about state that leaks between requests. We reach for it after the database work, not before.
LCP is usually an image or a hero heading. If it is an image, we make sure the browser finds it early and loads it first:
<img
src="/images/hero-1200.avif"
srcSet="/images/hero-800.avif 800w, /images/hero-1200.avif 1200w"
sizes="(max-width: 768px) 100vw, 1200px"
width={1200}
height={630}
fetchPriority="high"
alt="Order dashboard"
/>Three details in that tag do the work. fetchPriority="high" moves the image ahead of other downloads. srcSet and sizes stop phones from downloading the desktop file. width and height reserve the space, which prevents the layout shift that hurts CLS. We use loading="lazy" on images below the fold and never on the LCP image, since lazy loading the main image delays the metric you are trying to improve.
Web fonts are the other usual suspect. We self-host them, subset them to the characters the site uses, preload the one weight above the fold, and use font-display: swap so text shows in a fallback font instead of staying invisible.
INP problems come from the main thread being busy when the user clicks. Large bundles take time to parse and run, and heavy components re-render on every keystroke.
We look at the bundle with a visualizer and split routes and heavy widgets so they load when needed:
import {lazy, Suspense} from 'react'
const ReportChart = lazy(() => import('./ReportChart'))
export default function Reports(props: ReportsProps) {
return (
<Suspense fallback={<ChartSkeleton />}>
<ReportChart data={props.data} />
</Suspense>
)
}With Inertia and Vite, each page is already its own chunk, so the common problems are a large library imported on every page, a date library with all its locales, or a rich text editor loaded on pages that only display text. For typing lag, we check what re-renders with the React DevTools profiler. Filtering a list of 5,000 rows on every keystroke is slow regardless of framework, and useDeferredValue or moving the filter to the server fixes it.
Third-party scripts count too. Chat widgets, tag managers and analytics add up, and they run on the same thread as your app. We list every one, ask who uses its data, and remove the ones nobody can name an owner for.
Speed regresses quietly. A new feature adds a query in a loop, a new dependency doubles the bundle. Two things keep it in check for us: preventLazyLoading failing in development, and a Lighthouse run in CI on the most visited pages with a budget for LCP and bundle size, so a pull request that breaks the budget fails before review. Field data in Search Console takes about 28 days to reflect a change, so we compare it month over month rather than the day after a deploy.
Read also · Laravel
Database design decisions that save you later
Sep 1, 2026 · 8 min read
If your app is slow and you want someone to find out why and fix it, our product development team does this work on Laravel and React apps, either as a short engagement or as part of building the product.

Written by
Rainer Rodriguez
Co-founder, Nerdify
Rainer co-founded Nerdify in Managua, Nicaragua, in 2016 and works with the team on the client products it builds and maintains.
A software project scope lists the goal, the users, the flows with acceptance criteria, what is out, and how changes get handled. Our template inside.

Carlos Saldaña
Oct 6, 2026 · 7 min read
What building a custom web app involves, from scope to maintenance, what drives the cost, and when buying software off the shelf is the better call.

Carlos Saldaña
Aug 25, 2026 · 7 min read
Quality assurance is the set of habits that keep defects out of a release, from the ticket to production. Here is how a small team runs it.

Carlos Saldaña
Aug 11, 2026 · 7 min read