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 major version behind | Zielony |
| 2 majors behind (zwykle 6-12 mies. zaległości) | Żółty |
| 3+ majors behind | Czerwony, każdy zaległy major 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, real-world):
| 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 field data (real users), nie tylko lab data (Lighthouse local). Lab pokazuje co teoretycznie możliwe, field co dzieje się u użytkowników. Jeśli lab 90, a field 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 keywordzie. 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 keywordach, bez zmian w treści. Same Core Web Vitals.
5. Coverage tab — ile JavaScriptu jest martwe
Próg:
| Stan | Sygnał |
|---|---|
| > 50% unused JS przy initial paint | Żół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 unused JS to około 200ms parse time na średnim Androidzie plus bandwidth na rachunku użytkownika. W mobile-heavy traffic 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 bundle initial 840 KB. Coverage pokazał 78% unused. 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 version control (`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 version control 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 security | Żółty (warto audyt, zwykle OK) |
| Custom z `md5`/`sha1` haseł, JWT bez expiry, brak rate limit | 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 bugi, które znajdujemy regularnie:
- Brak rate limit na
/login(atak słownikowy w 30 sekund) - Reset hasła linkiem bez expiry (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 - Plain-text email confirmation z linkiem klikalnym (phishing magnet)
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 custom JWT bez expiry, stored 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% coverage przy nontrivialnym kodzie | Żółty |
| > 60% z testami integracyjnymi i E2E na critical paths | 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 critical paths (logowanie, zakup, formularz kontaktowy, payment, password reset)
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 critical paths (login, dodaj do koszyka, checkout, payment, faktura, anulowanie, password reset, wyszukiwanie). Czas ręcznego QA spadł z 4h na release do zera.
AI-slop signal w testach: Cursor pisze testy, które nie biegają, bo mock'ują 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 case studies 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 + coverage analysis + DB review + auth audit + 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.


