Editorial
18 aprilie 2026 anonym93 Studio laravel blade alpinejs

Cum sa treci de la React SPA la Blade + Alpine.js: case study real pe anonym93.dev

Am migrat anonym93.dev de la React SPA cu 500KB JavaScript la Blade + Alpine.js cu doar 68KB CSS si zero JS de aplicatie. Iata ce am invatat, ce a mers bine, si scorurile Core Web Vitals inainte si dupa.

Cum sa treci de la React SPA la Blade + Alpine.js: case study real pe anonym93.dev

De ce am migrat de la React la Blade + Alpine

Pana de curand, anonym93.dev era un React SPA clasic: Vite + React 18 + React Router v6 + TanStack Query + shadcn/ui. Toata partea publica (articole, portofoliu, pagini de prezentare) era randata client-side dupa ce browser-ul descarca si executa un bundle de ~500KB JavaScript.

Motivatia pentru migrare a venit din trei directii:

  • SEO slab: Google indexa partial paginile — continutul era vizibil doar dupa hydration
  • Core Web Vitals slabe: LCP peste 3 secunde pe mobile, INP peste 300ms
  • Complexitate inutila: aveam React + TanStack Query doar ca sa fetch-uim continut static din CMS

Solutia: Blade (Laravel) + Alpine.js pentru interactivitate minima. Vite ramane doar pentru CSS (Tailwind v4). Zero JavaScript de aplicatie.

Inainte vs dupa (numerele reale)

Bundle size

  • Inainte: ~500KB JS + 68KB CSS = 568KB critical assets
  • Dupa: 68KB CSS + ~15KB Alpine.js (CDN, cached) = 83KB
  • Reducere: 85%

Core Web Vitals (mobile, 4G simulat)

  • LCP: 3.2s → 0.9s
  • INP: 310ms → 45ms
  • CLS: 0.12 → 0.02
  • TTFB: 180ms → 250ms (SSR costa putin, dar merita)

Indexare Google

  • Inainte: ~40% pagini indexate corect (continut vizibil pentru crawler)
  • Dupa: 100% pagini indexate, continut complet in HTML initial

Planul de migrare

Migrarea a durat aproximativ 2 saptamani lucrate part-time. Pasii principali:

  1. Inventariere pagini React si componente
  2. Creare layout Blade + componente base (navbar, footer, hero)
  3. Rescriere pagina cu pagina (home, articles, catalog, detail pages)
  4. Migrare interactivitate catre Alpine.js (mobile menu, locale switcher, theme toggle)
  5. Eliminare dependente React si package.json cleanup
  6. Update teste si CI

Pasul 1: De la componente React la componente Blade

Exemplu concret — un card de articol. Inainte in React:

// resources/js/src/components/ArticleCard.tsx

export function ArticleCard({ article }: { article: Article }) {

return (

<a href=/articles/${article.slug}} className="group rounded-xl border p-6">

<h3 className="text-lg font-bold">{article.title}</h3>

<p className="text-sm text-muted">{article.excerpt}</p>

</a>

);

}

Dupa, in Blade:

{{-- resources/views/components/article-card.blade.php --}}

@props(['article'])

<a href="/articles/{{ $article['slug'] }}" class="group rounded-xl border p-6">

<h3 class="text-lg font-bold">{{ $article['translation']['title'] }}</h3>

<p class="text-sm text-muted">{{ $article['translation']['excerpt'] }}</p>

</a>

Apelul in pagina: <x-article-card :article="$article" />. Simplu, declarativ, randat server-side — zero JS.

Pasul 2: Interactivitatea — cand apelezi Alpine.js

Alpine.js acopera 90% din cazurile de interactivitate pe site-uri de prezentare: meniu mobile, dropdown-uri, modal-uri, tabs, toggle de tema, form-uri simple.

Exemplu — mobile menu toggle. Inainte avea nevoie de React state:

const [open, setOpen] = useState(false);

return (

<button onClick={() => setOpen(!open)}>Menu</button>

{open && <nav>...</nav>}

);

Dupa, in Alpine (o linie de cod):

<div x-data="{ open: false }">

<button @click="open = !open">Menu</button>

<nav x-show="open" x-transition>...</nav>

</div>

Alpine se incarca de pe CDN (14KB minified gzipped), fara build step, fara hydration.

Pasul 3: Fetching de date — de la TanStack Query la PHP Controller

Inainte, fiecare pagina facea fetch catre API:

const { data: articles } = useQuery({

queryKey: ['articles'],

queryFn: () => fetch('/api/cms/content?kind=ARTICLE').then(r => r.json()),

});

Dupa, in controller Laravel:

public function articles(Request $request)

{

$articles = ContentItem::query()

->where('kind', 'ARTICLE')

->where('status', 'PUBLISHED')

->with('translations')

->orderBy('sort_order')

->get();

return view('pages.articles', [

'articles' => $articles->toArray(),

]);

}

Datele ajung direct in view, randate server-side, cu zero round-trip de retea dupa load.

Pasul 4: Ce am sters (fara frica)

Dupa migrare, am sters aproximativ 98 de fisiere React:

  • Toate .tsx din resources/js/src/ (pages, components, hooks, lib, providers)
  • tsconfig.json, resources/js/app.tsx, resources/js/bootstrap.js
  • 45 dependencies din package.json: react, react-dom, react-router-dom, @tanstack/react-query, 19 pachete @radix-ui, lucide-react, shadcn utilities

package.json final are doar devDependencies pentru Tailwind, Vite si Laravel plugin — fara dependencies de runtime.

Pasul 5: CSS — Tailwind ramane

Partea buna: Tailwind v4 functioneaza identic in Blade. Nu a trebuit sa rescriu niciun clasa CSS. Singura modificare a fost in tailwind.config.js — am schimbat path-urile de scanare de la .tsx la .blade.php:

content: [

"./resources/views/**/*.blade.php",

// "./resources/js/src/**/*.{ts,tsx}", <-- sters

],

Capcane pe care le-am intalnit

1. Vite manifest in teste CI

Dupa migrare, testele PHPUnit esuau cu ViteManifestNotFoundException pentru ca Blade layout-ul folosea @vite('resources/css/app.css'), iar CI-ul nu rula npm run build. Fix: $this->withoutVite() in setUp() al testelor care randeaza pagini.

2. Artefactul de release nu includea lang/

Am avut un release in care fisierele lang/ro.json si lang/en.json lipseau din artefact, iar site-ul live afisa cheile de traducere (nav.home in loc de "Home"). Fix: adaugare lang in config/updater.php -> include_paths.

3. Middleware SetLocale nu se aplica pe 404

Paginile de eroare sunt randate de Laravel in afara de middleware stack-ul rutei. Am adaugat detectia locale inline in resources/views/errors/404.blade.php:

$locale = request()->query('locale')

?? request()->cookie('locale')

?? session('locale')

?? config('app.locale', 'ro');

app()->setLocale($locale);

Cand NU ar trebui sa faci migrarea asta

Nu e un glont magic. Nu migra daca:

  • Aplicatia ta e un dashboard cu multa stare si routing client-side
  • Ai echipa frontend specializata in React si nicio experienta PHP
  • Ai integrari complexe cu websockets sau streaming real-time
  • Ai investit masiv in componente React custom sau design system

Migrarea are sens pentru site-uri publice cu continut predominant static: bloguri, portofolii, e-commerce small, site-uri de prezentare.

Rezultate finale

  • ~98 fisiere React sterse, ~12.000 linii de cod eliminate
  • ~45 dependente npm sterse, node_modules de 3x mai mic
  • Build time scazut de la 18s la 2s
  • Core Web Vitals in zona verde pe toate metricile
  • Indexare Google 100% vs ~40% inainte
  • Paginat Filament admin neschimbat — continua sa mearga independent

Concluzie

Pentru site-ul meu, migrarea de la React SPA la Blade + Alpine.js a fost una dintre cele mai valoroase decizii tehnice din ultimul an. Mai putin cod de intretinut, performanta mai buna, SEO mai bun, build-uri rapide. Si cel mai important — pot sa scriu continut fara sa-mi fac griji ca e indexat corect.

Pentru setup-ul initial Laravel 12 + Vite + Tailwind v4, vezi Ghidul complet Laravel 12 + Vite + Tailwind v4.

Pentru comparatia detaliata intre SSR, SPA si SSG, citeste SSR vs SPA vs SSG — cand sa alegi fiecare.

Preferinte cookie

Setarile de mai jos controleaza doar analytics-ul local (GA4). Consimtamantul pentru Google Ads / AdSense trebuie gestionat separat prin Google Privacy & Messaging sau printr-un CMP certificat. Vezi detalii in Privacy Policy.