Wstęp
Co kilka tygodni rozmawiamy z kimś, kto kilka miesięcy temu zlecił stronę freelancerowi i teraz nie wie, co dalej. Strona działa, ale ledwo. Google nie indeksuje, ładowanie idzie sześć sekund, formularz nie wysyła maili. Pytanie pada zawsze podobnie: naprawić, czy napisać od nowa.
Uczciwa odpowiedź jest "to zależy", ale to słabo pomaga. Lepsza odpowiedź to osiem sygnałów, które oglądamy w każdym audycie. Jeśli wiesz, na co patrzeć, większość konsultacji można rozstrzygnąć w pół godziny.
Niżej rozpisuję każdy sygnał: jak go sprawdzić, jaki próg jest czerwony, co to znaczy w pieniądzach. Na końcu prosty framework: liczysz red flagi i dostajesz decyzję.
Krótka uwaga na początek. Piszę to z perspektywy stacku, którego sami używamy najczęściej: Next.js, TypeScript, Postgres. Dla Symfony, Rails czy Laravela logika jest ta sama, liczby trochę inne. Jeśli prowadzisz coś bardzo specyficznego (legacy COBOL, Magento, Salesforce Commerce), to nie jest artykuł dla Ciebie i nie pomoże Ci go przeczytać do końca.
1. TypeScript coverage i gęstość any
Próg:
| Stan | Sygnał |
|---|---|
| < 30% plików ma typy | Czerwony |
| 30-70%, lub `strict: false` | Żółty (typowo "dodano TS w połowie projektu i porzucono") |
| > 70% z `strict: true`, gęstość `any` < 1 na 100 linii | Zielony |
Jak sprawdzić w 5 minut:
1# Stosunek TS do JS2find src -name "*.ts" -o -name "*.tsx" | wc -l3find src -name "*.js" -o -name "*.jsx" | wc -l45# Gęstość any6rg ': any\b|as any\b' --type ts | wc -l78# Czy strict mode włączony9cat tsconfig.json | grep -A2 '"strict"'1011# Ile @ts-ignore / @ts-expect-error12rg '@ts-(ignore|expect-error)' --type ts | wc -l
Co to znaczy w pieniądzach: zmiana nazwy pola w API przy braku typów to dzień szukania w 12 plikach, każdy może kompilować się i wybuchać runtime. Z typami: kompilator pokazuje 12 miejsc w 30 sekund. Godzina seniora to 100-200 zł zależnie od stawki. W projekcie sześciomiesięcznym brak typów to dodatkowe 80-150h roboty na utrzymaniu, którą i tak ktoś zapłaci.
Konkret z naszej diagnostyki: zdarza nam się projekt Next.js 14 z tsconfig w którym strict: false, kilkaset wystąpień : any, kilkadziesiąt @ts-ignore. Na pierwszy rzut oka "ma TypeScript". W praktyce: typy nikogo nie chronią, każda zmiana to ruletka.
Dlaczego to jest dziś ważniejsze niż kilka lat temu: asystenty AI (Cursor, Copilot) chętnie dopisują function processData(data: any): any, jeśli strict jest wyłączony. Lint tego nie złapie. Rok takiej współpracy z asystentem i masz codebase, który "ma TypeScript", ale mógłby równie dobrze go nie mieć.
2. Wiek frameworka kontra aktualna stabilna wersja
Próg:
| Stan | Sygnał |
|---|---|
| 0-1 wersji głównej do tyłu | Zielony |
| 2 wersje główne do tyłu (zwykle 6-12 mies. zaległości) | Żółty |
| 3+ wersji głównych do tyłu | Czerwony, każda zaległa wersja to akumulowane CVE |
Jak sprawdzić:
1npx npm-check-updates2# albo bezpośrednio3cat package.json | jq '.dependencies'4# i porównać z npm view <pkg> version
Konkretne progi po frameworku (stan na 2026):
- Next.js: < 13 (brak App Router stabilnego, brak React 18 features)
- React: < 18 (brak Suspense, brak concurrent features)
- WordPress: < 6.0 (brak FSE, niezgodne z PHP 8.x)
- Vue: 2 (Vue 3 ma 5+ lat, Vue 2 EOL od końca 2023)
- Symfony LTS: < 6.4
Co to znaczy w pieniądzach: każda major version migration to typowo 2-3 tygodnie pracy seniora plus testowanie. Trzy zaległe majory to półtora-dwa miesiące samego upgrade'u, zanim cokolwiek nowego dodasz. Plus security: brak supportu znaczy, że każdy CVE pozostaje otwarty, dopóki nie zaktualizujesz.
Konkret: Fundacja Znajdki ze schronisk dla zwierząt przyszła do nas z Next.js 12, React 17, ostatni commit w package.json z 2023. Trzy majory Next, jeden major React. Sam upgrade to dwa tygodnie. Refaktor pod App Router to kolejne dwa. Razem miesiąc, zanim dodaliśmy nową funkcję.
3. Liczba wtyczek (WordPress) i zewnętrznych zależności (npm)
Próg:
| Stan | Sygnał |
|---|---|
| WordPress > 25 wtyczek aktywnych | Czerwony |
| npm > 150 deps top-level | Czerwony, > 300 = krytyczny |
| Initial JS bundle > 500 KB | Czerwony |
Jak sprawdzić:
1# WordPress2wp plugin list --status=active --format=count34# npm5cat package.json | jq '.dependencies | keys | length'67# Ile faktycznie używanych8npx depcheck910# Bundle size11npx next build # zobacz "First Load JS" w outpucie
Patterny do wykrycia w 5 minut:
- 6+ wtyczek SEO (Yoast + RankMath + AIOSEO razem, każda chce być pierwsza)
- 3+ wtyczek cache (W3 Total Cache + WP Rocket + LiteSpeed)
- Elementor + 4-5 add-onów (Elementor Pro + Essential Addons + Ultimate Addons)
- W repo npm:
lodash+ramda+underscorejednocześnie (3 biblioteki utility, każda 80 KB, każda używana w 2-3 miejscach)
Co to znaczy w pieniądzach: każda wtyczka to nowa powierzchnia dla CVE (średnio 1-2 critical CVE miesięcznie w top 50 plugin), każda to performance penalty (10-50 KB CSS+JS minimum), każda to potencjalny conflict z innymi przy upgrade'ach. Strona z 35 wtyczkami WP nie da się utrzymać przez 1 osobę poniżej połowy etatu.
Konkret: klient z e-commerce miał 47 aktywnych wtyczek WordPress. WooCommerce plus 12 jego rozszerzeń, 4 SEO, 3 cache, 8 marketingowych. PageSpeed mobile 32, LCP 8.7s. Pierwsza diagnoza: deactivate wszystkich i sprawdzenie samej strony bez nich. Wynik: LCP 2.1s. Czyli 6.6 sekundy ładowania to były same wtyczki. To jest moment, w którym refactor jest tańszy niż utrzymanie.
4. Lighthouse i Core Web Vitals
Próg (mobile, dane realne):
| Metryka | Czerwony |
|---|---|
| LCP | > 4s |
| INP | > 500ms |
| CLS | > 0.25 |
| Lighthouse score (mobile) | < 50, < 30 = krytyczny |
Jak sprawdzić:
1# Local lab data2npx lighthouse https://example.com --emulated-form-factor=mobile34# Field data (real users)5# pagespeed.web.dev → CrUX (Chrome User Experience Report)6# Search Console → Core Web Vitals
Klucz, który wielu pomija: patrz na dane terenowe (od realnych użytkowników), nie tylko dane laboratoryjne (Lighthouse lokalnie). Laboratorium pokazuje, co teoretycznie możliwe, teren – co dzieje się u użytkowników. Jeśli lokalnie 90, a w terenie 35, to znaczy, że Twój CDN, hosting albo geografia użytkowników psują doświadczenie, którego lokalnie nie zobaczysz.
Co to znaczy w pieniądzach: od 2024 Core Web Vitals są w kryteriach rankingowych Google (Page Experience). Strona z LCP 6s nie ma szans z konkurencją, która ma LCP 1.8s na tym samym słowie kluczowym. Plus konwersja: wedle badania Google z 2017 (SOASTA) 53% odwiedzin mobile zostaje porzuconych, jeśli ładowanie trwa dłużej niż 3 sekundy. Każda kolejna sekunda boli.
Konkret: klient z usługami medycznymi miał Lighthouse mobile 28, LCP 7.3s. Po trzech tygodniach refaktoru (usunięcie 14 wtyczek, optymalizacja obrazów, krytyczny CSS inline): Lighthouse 87, LCP 1.9s. Ruch z Google urósł 2x w 60 dni, na tych samych słowach kluczowych, bez zmian w treści. Same Core Web Vitals.
5. Zakładka Coverage – ile JavaScriptu jest martwe
Próg:
| Stan | Sygnał |
|---|---|
| > 50% niewykorzystanego JS przy pierwszym renderze | Żółty |
| > 70% | Czerwony |
| > 85% (typowy WordPress + Elementor) | Krytyczny |
Jak sprawdzić w 2 minuty:
- Otwórz Chrome DevTools (Cmd+Opt+I)
- Cmd+Shift+P, wpisz "Show Coverage"
- Kliknij record (czerwone kółko)
- Reload strony
- Stop record
- Sortuj po "Unused Bytes"
Co tam zobaczysz w typowym WordPress + Elementor:
- jQuery (90 KB) ładuje się dla strony bez ani jednego użycia jQuery
- 4 wersje React (Elementor add-onów, każdy z własną wersją)
- Cały Bootstrap CSS, gdy używana jest jedna klasa
lodash70 KB, gdy używasz tylko_.debounce
Co to znaczy w pieniądzach: każde 100 KB niewykorzystanego JS to około 200ms parsowania na średnim Androidzie plus transfer na rachunku użytkownika. Na ruchu zdominowanym przez mobile to znacząca konwersja, a w przypadku stron, które robią wiele żądań (SPA, dashboardy), efekt akumuluje się przy każdej nawigacji.
Konkret: klient na Next.js 13 z initial bundle 840 KB. Coverage pokazał 78% niewykorzystanego kodu. Powód: import * from 'lodash' w trzech miejscach, każde ściągało całą bibliotekę. Plus moment.js z całymi locales, z którego używana była jedna funkcja. Po zmianie na imports per-function i wymianie moment na date-fns: bundle 280 KB, LCP minus 2.1s. Jeden dzień pracy.
6. Architektura bazy danych
Próg:
| Stan | Sygnał |
|---|---|
| Schema w kontroli wersji (`prisma/`, `migrations/`) | Zielony |
| Schema istnieje, ale ALTER TABLE robione ręcznie w prod | Żółty |
| "Trzeba zapytać Tomka, on wie" | Czerwony |
Jak sprawdzić:
- Czy istnieje katalog
prisma/,migrations/,db/,supabase/migrations/? - Czy w README jest sekcja "Database setup"?
- Uruchom:
\d+ <table>w psql, czy są foreign keys, indeksy, constrainty?
Antypatterny do wyłapania:
- Tabele bez
created_at/updated_at - Brak foreign keys (relacje są w kodzie, ale nie wymuszone na poziomie DB)
- Kolumny
data1,data2,extra_field,comment_2,tag1,tag2... - JSON jako string w
VARCHAR(255)zamiastJSONB - Tabele po polsku obok angielskich, mieszane konwencje
- Hasło użytkownika w trzech tabelach jednego projektu nazwane:
pass,password_hash,hashed_password
Co to znaczy w pieniądzach: bez migracji w kontroli wersji każdy refaktor zaczyna się od archeologii. Co jest w prod, co w stagingu, co w devie, dlaczego się różnią. Zwykle tydzień samej diagnozy, zanim dotkniesz kodu aplikacyjnego. Plus nie da się odtworzyć środowiska, więc nowy developer dostaje sql dump z prod, co przy danych osobowych jest problemem RODO.
Konkret: projekt klienta z 2019, MySQL, brak migracji w git. 47 tabel, 23 z nich miały hasło w innej formie. Trzy tabele miały kolumny tag1, tag2, ..., tag10. Diagnoza zajęła nam 6 dni samego mappingu, zanim dotknęliśmy logiki aplikacyjnej.
7. Auth napisany od zera
Próg:
| Stan | Sygnał |
|---|---|
| Standardowa biblioteka (Auth.js, Supabase Auth, Clerk, Firebase) | Zielony |
| Custom z `bcrypt` + JWT, ze wszystkimi flagami bezpieczeństwa | Żółty (warto audyt, zwykle OK) |
| Custom z `md5`/`sha1` haseł, JWT bez czasu wygaśnięcia, brak ograniczenia liczby żądań | Czerwony |
Jak sprawdzić:
1# Czy jest standardowa biblioteka2cat package.json | grep -E "next-auth|@auth|@supabase/auth|@clerk|firebase-auth"34# Czy custom5rg "bcrypt|argon2|scrypt" --type ts --type js67# Hashing dziadowski8rg "createHash\('md5'|createHash\('sha1'" --type ts --type js
Konkretne błędy, które znajdujemy regularnie:
- Brak rate limit na
/login(atak słownikowy w 30 sekund) - Reset hasła linkiem bez czasu wygaśnięcia (token żyje wiecznie)
- Reset hasła przez HTTP zamiast HTTPS (gdy ktoś zrobił reverse proxy bez przemyślenia)
- JWT w
localStorage(XSS w jednym formularzu = przejęcie konta na zawsze) - Cookies bez
Secure,HttpOnly,SameSite=Lax - Potwierdzenie e-maila plain text z linkiem klikalnym (magnes na phishing)
Co to znaczy w pieniądzach (i nie tylko): custom auth to ryzyko CVE, ryzyko skargi do UODO (RODO art. 32, środki techniczne), ryzyko incydentu, który dla SaaS B2B oznacza realną szansę na utratę firmy reputacyjnie. Standardowa biblioteka znaczy, że ktoś inny utrzymuje, ktoś inny audytuje, łatwiej spełnić obowiązki RODO i NIS2.
Konkret: widzieliśmy SaaS B2B z customowym JWT bez czasu wygaśnięcia, trzymanym w localStorage. XSS w jednym z formularzy oznaczałby, że atakujący ma dostęp do konta klienta na zawsze, bez możliwości unieważnienia. Naprawa „z doskoku” niemożliwa bez przepisania wszystkich endpointów. To jest prawdziwy moment, w którym tylko rewrite ma sens, niezależnie od pozostałych sygnałów.
Na co uważać przy współpracy z asystentem AI: Cursor przy "dodaj logowanie" zaproponuje bcrypt + JWT, jeśli sam go nie poprowadzisz. Nie wie, że masz już Supabase z Auth gotowym do użycia. Jeśli developer kopiuje sugestię bez weryfikacji, dostajesz custom auth wtedy, kiedy go nie chciałeś.
8. Testy i CI/CD
Próg:
| Stan | Sygnał |
|---|---|
| 0 testów | Czerwony, każda zmiana to ruletka |
| < 30% pokrycia testami przy nietrywialnym kodzie | Żółty |
| > 60% z testami integracyjnymi i E2E na ścieżkach krytycznych | Zielony |
Jak sprawdzić:
1# Czy są testy2find . -name "*.test.*" -o -name "*.spec.*" | wc -l34# Czy jest CI5ls -la .github/workflows/ .gitlab-ci.yml 2>/dev/null67# Coverage report (jeśli skonfigurowany)8npm test -- --coverage
Co weryfikować poza istnieniem:
- Czy testy faktycznie się uruchamiają (często jest folder
__tests__z pięcioma plikami, które rzucają błąd przynpm test) - Czy CI faktycznie blokuje merge przy fail (
required checkw settings) - Czy są testy E2E (Playwright/Cypress) na ścieżkach krytycznych (logowanie, zakup, formularz kontaktowy, płatność, reset hasła)
Co to znaczy w pieniądzach: bez testów refaktor jest niemożliwy bez regresji. Każda zmiana to ręczne klikanie po stronie. W projekcie sześciomiesięcznym brak CI/CD to dodatkowe 40-60h ręcznego QA, które nikt nie wycenił, a ktoś musi zrobić.
Konkret: klient z e-commerce, zero testów, zero CI. Pierwsza naprawa po migracji złamała coś w panelu admina, zauważone trzy dni później przez klientów telefonicznie. Drugi miesiąc po migracji dodaliśmy Playwright na 8 ścieżkach krytycznych (logowanie, dodaj do koszyka, checkout, płatność, faktura, anulowanie, reset hasła, wyszukiwanie). Czas ręcznego QA spadł z 4h na wydanie do zera.
AI-slop w testach: Cursor pisze testy, które nie działają, bo mockują wszystko, w tym funkcję, którą rzekomo testują. Łatwo poznać: test ma expect(true).toBe(true) jako asercję albo cała logika domeny jest zamockowana. Test się "uda", regresji nie wykryje.
Framework decyzyjny
Policz red flags z 8 sygnałów. Każdy jeden punkt.
| Liczba red flags | Decyzja | Czas naprawy | Czas rewrite |
|---|---|---|---|
| 0-2 | Naprawa, oszczędzasz 60-80% kosztu | 2-6 tyg. | 4-6 mies. |
| 3-5 | Kalkulacja, decyzja zależy od deadline i budżetu | 8-16 tyg. | 4-6 mies. |
| 6-8 | Tylko rewrite, ale w kawałkach < 3 mies. (Strangler Pattern), nie big bang | n/a | 4-8 mies. w kawałkach |
Big bang rewrite (cały system od zera, jednym podejściem) ma sens tylko wtedy, gdy strona ma mniej niż 5 podstron, niski ruch i brak integracji zewnętrznych. We wszystkich innych przypadkach Strangler Pattern wygrywa: stawiasz nowy stack obok starego, migrujesz po jednym module, stary system gaśnie naturalnie. Wymaga to dobrego routingu i koordynacji, ale daje Ci możliwość zatrzymania w dowolnym momencie, jeśli coś się posypie.
Co dalej
Trzy ścieżki, zależnie od tego, gdzie jesteś.
Jeśli czytasz to z ciekawości i chcesz głębiej: tu jest pełny raport (22 min, dwa studia przypadku krok po kroku, kontekst rynkowy, kiedy WordPress nadal ma sens).
Jeśli masz konkretną stronę i chcesz subiektywnej oceny: wyślij URL na audyt@epko.tech z dopiskiem "8 sygnałów". W 48h odeślę Ci, jak wypada w każdym z ośmiu punktów. Bez zobowiązań, bez handlowca, jedna strona A4.
Jeśli wynik jest zły i chcesz pełny audyt techniczny: Lighthouse + analiza coverage + przegląd bazy + audyt auth + plan remediacji w 5 dni roboczych. Cena zależy od skali, da się ją oszacować po 15-minutowej rozmowie. Umów się.
Jedno na koniec. Te 8 sygnałów daje Ci 70% odpowiedzi w pół godziny. Pozostałe 30% to rozmowa, w której pytamy o rzeczy, których nie da się sprawdzić skanerem: Twój roadmap produktowy, planowane integracje, kompetencje zespołu, deadline. Najtańszy refaktor to ten, którego nie robimy, jeśli i tak odejdziesz od tej strony za pół roku.


