Deploying Next.js to Vercel takes about ninety seconds. Deploying it well takes an afternoon, and the difference shows up in your bill and your Core Web Vitals rather than in whether the site loads.
The basic deploy
- Push your project to GitHub
- Import the repository at vercel.com
- Vercel detects Next.js and sets the build command automatically
- Deploy
Every push to your default branch now deploys to production, and every pull request gets its own preview URL. The preview URLs are genuinely the best part — send one to a client instead of describing a change.
The root api/ folder trap
This one catches people and produces no error, only a warning most never read.
Vercel treats a folder named api/ at the project root as its own Serverless Functions directory, entirely separate from Next.js. If you keep plain data files there — arrays of products, blog posts, configuration — Vercel compiles each one into a serverless function that does nothing:
Compiling "blogs.js" from ESM to CommonJS...
Compiling "products.js" from ESM to CommonJS...Those become publicly routable endpoints that error at runtime, and they count against your function limits. Next.js API routes belong in pages/api/ (or app/api/). Plain data belongs in data/ or lib/ — anywhere except a root api/.
Environment variables
Set them in the project settings, not in a committed file.
- Variables prefixed NEXT_PUBLIC_ are embedded in the browser bundle. Never put a secret behind that prefix.
- Everything else is server-only and safe.
- They are set per environment: production, preview and development. A variable set only for production will be missing in previews.
- Changing a variable does not update existing deployments — you must redeploy.
Commit a .env.example listing the names with empty values so anyone cloning the repo knows what is required.
Custom domains
Add the domain in project settings, then point DNS at Vercel. For an apex domain use the A record they provide; for www use a CNAME. SSL is issued automatically and renews itself.
Pick one canonical host — either example.com or www.example.com — and set the other to redirect. Serving both means duplicate content and split ranking signals.
Caching headers that actually matter
Static assets under /_next/static are hashed and cached forever automatically. Files you put in public/ are not — they get a short cache by default, so returning visitors re-download your images.
// next.config.js
async headers() {
return [{
source: '/images/:path*',
headers: [{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
}],
}];
}Only do this for files whose names change when the content changes. If you overwrite hero.jpg in place, a one-year immutable cache means nobody sees the new one.
Static, server-rendered, or ISR?
This choice affects both speed and cost.
- Static (getStaticProps) — built once, served from the CDN. Fastest and effectively free. Use it for anything that is not per-request.
- ISR (getStaticProps with revalidate) — static, but regenerated in the background on a schedule. Right for content that updates occasionally.
- Server-rendered (getServerSideProps) — runs a function on every request. Slower and billable. Use only when the response genuinely depends on the request.
The common mistake is getServerSideProps on pages that could be static. It works, it is just slower and it costs money for no benefit.
Image optimisation and cost
Vercel optimises next/image requests and bills for source images transformed. Two things keep that number down:
- Compress source images before uploading. Optimisation resizes; it cannot fix a badly exported 2 MB PNG.
- Limit deviceSizes in next.config.js to widths you actually use. Every extra size is another cached transformation.
Things worth turning on
- Security headers — X-Content-Type-Options, Referrer-Policy, X-Frame-Options — via the headers() config
- poweredByHeader: false, so you stop advertising your stack
- Vercel Analytics if you want Core Web Vitals from real users rather than lab data
- Deployment protection on preview URLs if the site is not public yet
Watch the build log
It tells you things nobody reads:
- Warnings about misplaced folders, like the api/ issue above
- Notices that your framework version has known vulnerabilities
- Which routes are static, SSG or dynamic — a quick sanity check that your rendering strategy is what you intended
- Bundle sizes per route, which is where you notice a dependency creeping in
Reading the build output after every significant change is a cheap habit that catches problems before your users do.
