
Διόρθωση του INP σε πραγματική εφαρμογή React: ανάλυση των αργών αλληλεπιδράσεων που εντοπίσαμε και των αλλαγών μας
Αποκατάσταση του INP σε πίνακα B2B στην παραγωγή. Οι μετρήσεις, οι τέσσερις αλληλεπιδράσεις που έστελναν το 75ο εκατοστημόριο στο κόκκινο και οι διορθώσεις με React 18 και αλλαγές αρχιτεκτονικής — useDeferredValue, μεταβάσεις, ανάθεση συμβάντων, εικονικοποίηση, όρια hydration — που έφεραν την εφαρμογή εντός ορίων.
- Συντάκτης
- Από το DevLume
- Δημοσίευση
- Δημοσιεύτηκε στις 3 Οκτωβρίου 2026
Βασικά συμπεράσματα
- Το INP έγινε Core Web Vital τον Μάρτιο του 2024, αντικαθιστώντας το FID — και μετριέται στο 75ο εκατοστημόριο όλων των αλληλεπιδράσεων κλικ, αφής και πληκτρολογίου στη σελίδα, όχι μόνο της πρώτης εισόδου (web.dev).
- Παγκοσμίως, το 77% των σελίδων σε κινητά έχει «καλό» INP το 2025, έναντι 55% το 2022 — αλλά οι δευτερεύουσες σελίδες σε κινητά βρίσκονται στο 69%, με διαφορά 11 μονάδων από τις αρχικές (Web Almanac 2025).
- Το INP έχει τρεις φάσεις — καθυστέρηση εισόδου, διάρκεια επεξεργασίας, καθυστέρηση παρουσίασης — και οι χειρότερες αλληλεπιδράσεις ενός τυπικού πίνακα React επηρεάζουν τουλάχιστον δύο (web.dev).
- Τα φθηνότερα οφέλη είναι συνήθως δομικά: μεταφέρετε μη απαραίτητη εργασία εκτός χειριστών συμβάντων, χαρακτηρίστε τις ενημερώσεις κατάστασης μεγάλης εμβέλειας ως μεταβάσεις και εικονικοποιήστε τις μεγάλες λίστες. Τα ακριβότερα οφέλη είναι αρχιτεκτονικά: απομακρύνετε πλήρως εργασία από την κρίσιμη διαδρομή.
- Τα
useTransitionκαιuseDeferredValueλύνουν διαφορετικά προβλήματα. Οι μεταβάσεις είναι διακόψιμες ενημερώσεις κατάστασης που ελέγχετε· οι αναβαλλόμενες τιμές προορίζονται για εισόδους των οποίων τη συνάρτησηsetδεν ελέγχετε (React docs).
Συνοπτικά
Η εφαρμογή ήταν ένας πίνακας αναλύσεων B2B για πελάτη με μερικές εκατοντάδες ταυτόχρονους χρήστες. Το INP στο 75ο εκατοστημόριο βρισκόταν περίπου στα 540ms σε κινητές συσκευές μεσαίας κατηγορίας — ξεκάθαρα στην κόκκινη ζώνη της Google. Το 75ο εκατοστημόριο είναι το όριο που μετρά, επειδή αντιπροσωπεύει την πιο αργή αλληλεπίδραση που θα βιώσει μια τυπική συνεδρία, όχι έναν μέσο όρο. Τέσσερις αλληλεπιδράσεις ευθύνονταν σχεδόν για όλη την ουρά μεγάλων καθυστερήσεων: μια πλευρική στήλη φίλτρων που απέδιδε ξανά ολόκληρο το πλέγμα αποτελεσμάτων σε κάθε κλικ πλαισίου ελέγχου, ένα πεδίο αναζήτησης που εκτελούσε σύγχρονη ασαφή αντιστοίχιση στις αποθηκευμένες γραμμές, ένας επιλογέας χρονικού διαστήματος που ανακτούσε και αναδιέτασσε γειτονικά γραφήματα στην ίδια εργασία και ένα μενού ενεργειών γραμμής που επαναπροσαρτιόταν σε κάθε άνοιγμα. Μετά από τρεις εβδομάδες εργασίας φτάσαμε περίπου στα 180ms p75. Καμία διόρθωση δεν ήταν εξεζητημένη. Το λάθος ήταν ότι ο αρχικός κώδικας αντιμετώπιζε το React σαν να ομαδοποιεί τα πάντα δωρεάν και το κύριο νήμα σαν να έχει απεριόριστη χωρητικότητα.
Τι εξετάζαμε
Η εφαρμογή ήταν ένας πίνακας Next.js 14 που, μετά την αρχική απόκριση του διακομιστή, αποδιδόταν κυρίως ως SPA στον πελάτη και επικοινωνούσε με backend tRPC. Περίπου 12,000 ενεργές γραμμές στην κύρια προβολή ανά πάσα στιγμή, συν τέσσερα υποστηρικτικά γραφήματα που ενημερώνονταν όταν άλλαζαν τα φίλτρα. Οι αναφορές σφαλμάτων του πελάτη έλεγαν «το πεδίο αναζήτησης μοιάζει χαλασμένο» και «πρέπει να πατήσω το φίλτρο δύο φορές», όχι «η σελίδα είναι αργή». Και τα δύο είναι κλασικά συμπτώματα INP. Ο χρήστης πατά, τίποτα δεν αλλάζει ορατά για μισό δευτερόλεπτο, υποθέτει ότι το πάτημα δεν καταγράφηκε και ξαναπατά. Το INP αποτυπώνει ακριβώς αυτή την καθυστέρηση: το web.dev το ορίζει ως τον χρόνο από «την έναρξη της αλληλεπίδρασης μέχρι τη στιγμή που παρουσιάζεται πλήρως το επόμενο καρέ» (web.dev).
Η μετρική είναι επίσης πιο αυστηρή από όσο συνήθως θυμόμαστε. Το INP δεν είναι ο μέσος όρος μιας συνεδρίας — είναι περίπου η χειρότερη αλληλεπίδραση, με εξαίρεση ορισμένες ακραίες τιμές. Η προδιαγραφή χρησιμοποιεί τη μεγαλύτερη παρατηρημένη αλληλεπίδραση για σύντομες συνεδρίες και μεταβαίνει σε εκτιμητή κοντά στο μέγιστο για συνεδρίες με πολλές αλληλεπιδράσεις. Έτσι, ένα αργό κλικ φίλτρου σε μια συνεδρία αναλυτή τριάντα λεπτών αρκεί για να ορίσει το INP της σελίδας για αυτόν τον χρήστη. Ένας καλός μέσος όρος δεν εξουδετερώνει μία κακή αλληλεπίδραση.
Πώς μετρήσαμε
Τρία εργαλεία, με αυτή τη σειρά:
Πρώτα, παρακολούθηση πραγματικών χρηστών. Η βιβλιοθήκη web-vitals έστελνε δείγματα INP στη ροή αναλυτικών δεδομένων μας. Μας έδειχνε ποιες διαδρομές είχαν πρόβλημα και ποιες αλληλεπιδράσεις ευθύνονταν, μέσω των event.target και event.type της πιο αργής αλληλεπίδρασης κάθε συνεδρίας. Χωρίς αυτά θα μαντεύαμε ποια από δεκάδες διαδραστικά στοιχεία είχαν σημασία. Τα δεδομένα πραγματικής χρήσης είναι τα μόνα που χρησιμοποιεί πράγματι ο αλγόριθμος κατάταξης της Google· τα εργαστηριακά δεδομένα χρησιμεύουν στην αποσφαλμάτωση.
Δεύτερο, το πλαίσιο Performance του Chrome DevTools. Όταν ξέραμε ποια αλληλεπίδραση να αναπαραγάγουμε, η λωρίδα αλληλεπιδράσεων του Performance έδινε την ανάλυση — καθυστέρηση εισόδου, εργασίες σεναρίων κατά την επεξεργασία και εργασίες απόδοσης και σχεδίασης στο τέλος. Το μοντέλο τριών φάσεων του web.dev αντιστοιχεί άμεσα σε όσα βλέπετε εκεί: «καθυστέρηση εισόδου (πριν αρχίσουν οι συναρτήσεις συμβάντων), διάρκεια επεξεργασίας (εκτέλεση των συναρτήσεων συμβάντων) και καθυστέρηση παρουσίασης (χρόνος μέχρι το επόμενο καρέ να εμφανίσει οπτικά αποτελέσματα)» (web.dev).
Τελευταίο και με φειδώ, το React Profiler. Δείχνει καλά ποια components αποδόθηκαν ξανά και περίπου γιατί, αλλά υπερεκτιμά το κόστος απόδοσης σε σχέση με τους πραγματικούς αριθμούς του Performance και δεν εμφανίζει εργασία εκτός React. Χρησιμοποιήστε το για να απαντήσετε «αποδόθηκε ξανά αυτό το component;», όχι «πόσο αργή ήταν η αλληλεπίδραση;».
Μια σημείωση για το PageSpeed Insights: είναι χρήσιμος έλεγχος λογικότητας επειδή αντλεί δεδομένα πραγματικής χρήσης 28 ημερών από το CrUX, αλλά δεν προσφέρεται για διαδοχικές δοκιμές βελτίωσης. Τα δεδομένα είναι πολύ συγκεντρωτικά και καθυστερημένα. Χρησιμοποιήστε το για να επιβεβαιώσετε έναν μήνα αργότερα ότι η διόρθωση άντεξε, όχι για να καθοδηγήσετε την αποσφαλμάτωση.
Οι τέσσερις αλληλεπιδράσεις που μας κόστιζαν
1. Η πλευρική στήλη φίλτρων που απέδιδε ξανά όλο το πλέγμα
Η αρχική υλοποίηση κρατούσε ένα ενιαίο αντικείμενο filters σε React Context στην κορυφή της σελίδας. Κάθε αλλαγή πλαισίου ελέγχου καλούσε το setFilters με νέο αντικείμενο, ακυρώνοντας την απομνημόνευση σχεδόν κάθε component που ήταν συνδρομητής στο context — μαζί και του πλέγματος 12,000 γραμμών, που επανεκτελούσε σύγχρονα τη λογική φιλτραρίσματος μέσα στον χειριστή συμβάντος. Χρόνος επεξεργασίας ανά κλικ: 380-520ms σε συσκευές μεσαίας κατηγορίας.
Η διόρθωση είχε δύο μέρη. Πρώτον, διαχωρίσαμε το context ώστε το πλέγμα να παρακολουθεί μια παράγωγη τιμή visibleRows αντί για την ακατέργαστη κατάσταση φίλτρων και υπολογίζαμε το visibleRows με useMemo που εξαρτιόταν από το αντικείμενο φίλτρων. Φθηνή αλλαγή, προφανής εκ των υστέρων. Δεύτερον — και αυτό έφερε το μεγαλύτερο όφελος — τυλίξαμε την ενημέρωση φίλτρων σε startTransition:
const [isPending, startTransition] = useTransition()
function onFilterChange(next: FilterState) {
startTransition(() => setFilters(next))
}Αυτό κάνει δύο πράγματα. Χαρακτηρίζει την επακόλουθη επαναπόδοση ως διακόψιμη και μας επιτρέπει να εμφανίζουμε μια διακριτική ένδειξη isPending στο πλέγμα χωρίς να εμποδίζουμε την άμεση ενημέρωση του ίδιου του πλαισίου ελέγχου. Η τεκμηρίωση του React είναι σαφής για την αξία του: «όσο μια μετάβαση εξελίσσεται, η διεπαφή παραμένει αποκρίσιμη… αν ο χρήστης πατήσει μια καρτέλα, αλλάξει γνώμη και πατήσει άλλη, το δεύτερο κλικ θα αντιμετωπιστεί αμέσως χωρίς αναμονή ολοκλήρωσης της πρώτης ενημέρωσης» (React docs).
Το πλαίσιο ελέγχου ενημερώνεται πλέον στο επόμενο καρέ, ανεξάρτητα από την εργασία του πλέγματος. Το πλέγμα ακολουθεί όταν μπορέσει.
2. Το πεδίο αναζήτησης που εκτελούσε σύγχρονη ασαφή αντιστοίχιση
Ίδιο μοτίβο συμπτώματος, διαφορετική αιτία. Το πεδίο αναζήτησης αποθήκευε την τιμή του στην κατάσταση του component και ένα effect επανεκτελούσε την ασαφή αντιστοίχιση σε κάθε αλλαγή. Επειδή το πεδίο ήταν ελεγχόμενο, κάθε πληκτρολόγηση προκαλούσε επείγουσα ενημέρωση κατάστασης, η οποία μετέφερε την αντιστοίχιση στην επόμενη απόδοση, που εκτελούνταν στο κύριο νήμα πριν από την επόμενη σχεδίαση.
Το useTransition δεν βοηθά άμεσα εδώ — δεν μπορείτε να τυλίξετε το setState ενός ελεγχόμενου πεδίου σε μετάβαση χωρίς να καθυστερήσετε και την απόκριση του ίδιου του πεδίου. Αυτή είναι ακριβώς η περίπτωση για την οποία υπάρχει το useDeferredValue. Η τεκμηρίωση του React το λέει ευθέως: «αν θέλετε να ξεκινήσετε μια μετάβαση ως απόκριση σε prop ή τιμή προσαρμοσμένου Hook, δοκιμάστε το useDeferredValue» (React docs).
Αλλάξαμε το component αναζήτησης ώστε η δαπανηρή αντιστοίχιση να διαβάζει ένα αναβαλλόμενο αντίγραφο του ερωτήματος:
const [query, setQuery] = useState("")
const deferredQuery = useDeferredValue(query)
const results = useMemo(
() => fuzzyMatch(rows, deferredQuery),
[rows, deferredQuery]
)Το ίδιο το πεδίο διατηρεί υψηλή προτεραιότητα. Η λίστα αποτελεσμάτων ενημερώνεται στο επόμενο διαθέσιμο καρέ μετά την πληκτρολόγηση. Αυτή η αλλαγή μόνη της κατέβασε το p75 της αναζήτησης από ~410ms σε λιγότερα από 100ms.
3. Ο επιλογέας χρονικού διαστήματος που έκανε υπερβολική δουλειά σε μία εργασία
Όταν ο χρήστης επέλεγε νέο χρονικό διάστημα, τρία πράγματα συνέβαιναν στον ίδιο χειριστή συμβάντος: κλήση tRPC για επανάκτηση του κύριου συνόλου δεδομένων, επανυπολογισμός τεσσάρων συγκεντρώσεων για τα υποστηρικτικά γραφήματα όταν επέστρεφαν τα δεδομένα και επαναπόδοση κάθε γραφήματος με ομαλή κινούμενη μετάβαση. Η ανάκτηση ήταν ήδη ασύγχρονη, αλλά όλη η επακόλουθη εργασία — συγκέντρωση, διάταξη, προετοιμασία μετάβασης — κατέληγε σε μία μεγάλη εργασία μόλις ολοκληρωνόταν η υπόσχεση.
Η διόρθωση ήταν απλή: διαχωρισμός. Μεταφέραμε τις συγκεντρώσεις των γραφημάτων πίσω από έναν μικρό βοηθητικό μηχανισμό τύπου scheduler, που παραχωρούσε τον έλεγχο στον φυλλομετρητή ανάμεσα στα γραφήματα:
async function recomputeCharts(dataset: Dataset) {
for (const chart of chartIds) {
await yieldToMain()
aggregateAndSet(chart, dataset)
}
}
const yieldToMain = () =>
new Promise<void>((resolve) => setTimeout(resolve, 0))Το setTimeout(0) αρκεί σε όλους τους φυλλομετρητές που χρησιμοποιούν οι πελάτες μας. Αν έχετε πρόσβαση στο scheduler.yield() και θέλετε την καλύτερη σημασιολογία του, χρησιμοποιήστε το όπου υποστηρίζεται, αλλά η απλή εκδοχή αρκεί στις περισσότερες περιπτώσεις. Η καθοδήγηση του web.dev είναι γενική για συγκεκριμένο λόγο: «διαχωρίστε την εργασία των συναρτήσεων συμβάντων σε ξεχωριστές εργασίες. Αυτό εμποδίζει τη συνολική εργασία να γίνει μία μεγάλη εργασία» (web.dev).
Αυτή ήταν η μεγαλύτερη μεμονωμένη βελτίωση του p75, επειδή ο επιλογέας χρονικού διαστήματος ήταν η πιο προβληματική αλληλεπίδρασή μας.
4. Το μενού ενεργειών γραμμής που επαναπροσαρτιόταν
Το αναδυόμενο μενού ενεργειών γραμμής ήταν ένα portal που προσαρτιόταν στο DOM την πρώτη φορά που άνοιγε σε κάθε γραμμή και αποπροσαρτιόταν στο κλείσιμο. Το άνοιγμα εκτελούσε ολόκληρη την αρχικοποίησή του — μέτρηση διάταξης, διαχείριση εστίασης, σύνδεση ARIA — μέσα στον χειριστή κλικ. Η καθυστέρηση p75 από το κλικ ως το άνοιγμα ήταν περίπου 220ms.
Το αντικαταστήσαμε με ένα μοναδικό αναδυόμενο μενού στο επίπεδο της σελίδας, με στόχο portal που επανατοποθετούνταν στη γραμμή του κλικ. Το μενού παραμένει προσαρτημένο· αλλάζουν μόνο η θέση και το περιεχόμενό του. Ο χρόνος από το κλικ ως το άνοιγμα έπεσε κάτω από 60ms. Σε τέτοιες διορθώσεις το React Profiler δεν βοηθά, επειδή το κόστος βρίσκεται στη μέτρηση του DOM και στην εγκατάσταση χειριστών συμβάντων, όχι στην απόδοση. Το Performance είναι το μόνο εργαλείο που δείχνει την πραγματική εικόνα εδώ.
Οι αρχιτεκτονικές διορθώσεις που συνέβαλαν παράλληλα
Δύο αλλαγές αφορούσαν τη μείωση της βασικής εργασίας στο κύριο νήμα και όχι συγκεκριμένες αλληλεπιδράσεις.
Εικονικοποίηση του πλέγματος. Το πλέγμα 12,000 γραμμών απέδιδε κάθε γραμμή στο DOM, με απόκρυψη μέσω overflow: hidden και κύλισης CSS. Ακόμη και χωρίς επαναπόδοση, ήταν υποδέντρο 12,000 κόμβων για το οποίο ο φυλλομετρητής έπρεπε να διατηρεί στυλ και διάταξη. Η μετάβαση σε εικονικοποίηση ορατού παραθύρου περιόρισε τις αποδιδόμενες γραμμές περίπου στις 40-60 ανά πάσα στιγμή. Το INP βελτιώθηκε σε κάθε αλληλεπίδραση της σελίδας, όχι μόνο σε όσες αφορούσαν το πλέγμα, επειδή το κύριο νήμα είχε λιγότερη πάγια εργασία να ανταγωνιστεί.
Περιορισμός των ορίων hydration. Η διαδρομή ήταν σελίδα «use client» από τη ρίζα της, οπότε όλο το διαδραστικό δέντρο υφίστατο hydration στην πρώτη φόρτωση και αύξανε την καθυστέρηση εισόδου αμέσως μετά. Μεταφέραμε το "use client" χαμηλότερα, στα πραγματικά διαδραστικά components — πλευρική στήλη φίλτρων, πεδίο αναζήτησης, χειριστήρια γραφημάτων — και αφήσαμε τον στατικό σκελετό και την αρχική απόδοση γραφημάτων στον διακομιστή. Η σχετική διατύπωση του web.dev: «η αξιολόγηση σεναρίων… μπορεί να εισαγάγει μεγάλες εργασίες στο κύριο νήμα, καθυστερώντας την απόκριση του φυλλομετρητή σε άλλες αλληλεπιδράσεις χρήστη» (web.dev). Λιγότερο σενάριο κατά το hydration σήμαινε μικρότερη καθυστέρηση εισόδου στα πρώτα δέκα δευτερόλεπτα της συνεδρίας, όταν οι περισσότεροι χρήστες πατούν το πρώτο φίλτρο.
Πού κατέληξαν οι μετρήσεις
Πριν: INP στο 75ο εκατοστημόριο περίπου 540ms στις επηρεαζόμενες διαδρομές, με την πλευρική στήλη φίλτρων και τον επιλογέα χρονικού διαστήματος να ευθύνονται για το μεγαλύτερο μέρος των ακραίων καθυστερήσεων. Μετά: περίπου 180ms p75, σταθερά σε έναν μήνα δεδομένων πραγματικής χρήσης. Ο ιστότοπος πέρασε από «κακό» σε «καλό» στην κλίμακα INP της Google (web.dev).
Για σύγκριση: το Web Almanac 2025 κατέγραψε παγκοσμίως «καλό INP» στο 77% των σελίδων σε κινητά, με τις αρχικές στο 80% και τις δευτερεύουσες στο 69% — δηλαδή οι εσωτερικές σελίδες με πολλές αναλύσεις, όπως αυτή, τείνουν να είναι το σημείο όπου ξεφεύγει η μετρική (Web Almanac 2025). Η επίδοση κάτω από 200ms σε μια πυκνή εσωτερική προβολή είναι η δυσκολότερη περίπτωση και εκείνη που μετρά για λογισμικό B2B, όπου οι χρήστες περνούν ολόκληρες συνεδρίες μέσα στην ίδια σελίδα.
Τι θα κάναμε διαφορετικά
Δύο πράγματα, με σειρά του πόσο μετανιώνουμε που δεν τα κάναμε νωρίτερα.
Θα προσθέταμε το σενάριο RUM του web-vitals την πρώτη εβδομάδα, όχι την τρίτη. Χωρίς δεδομένα πραγματικής χρήσης, περάσαμε το πρώτο sprint βελτιστοποιώντας σημεία που ήταν ήδη επαρκή, επειδή ξεχώριζαν στο Performance. Τα δεδομένα πραγματικής χρήσης μάς οδήγησαν στους πραγματικούς ενόχους μέσα σε μία ημέρα από την εγκατάστασή τους.
Θα γράφαμε εξαρχής τα components σύμφωνα με το μοντέλο ταυτόχρονης εκτέλεσης του React. Οι περισσότερες διορθώσεις ήταν αλλαγές μίας γραμμής που τύλιγαν ενημερώσεις κατάστασης σε startTransition ή αντικαθιστούσαν ανάγνωση κατάστασης με useDeferredValue. Αν η ομάδα ρωτούσε συστηματικά στις ανασκοπήσεις κώδικα «είναι αυτή η ενημέρωση επείγουσα ή διακόψιμη;», η εφαρμογή δεν θα είχε φτάσει εξαρχής στην κόκκινη ζώνη. Η λογική είναι απλή: ό,τι πληκτρολογεί ή πατά απευθείας ο χρήστης είναι επείγον· ό,τι ακολουθεί — φιλτράρισμα, ταξινόμηση, επανυπολογισμός, επανασχεδίαση — είναι σχεδόν πάντα μετάβαση.
Το INP είναι το Core Web Vital με τις περισσότερες αποτυχίες το 2026 επειδή τα παραπάνω μοτίβα δεν αποτελούν ακόμη συνήθη πρακτική στις περισσότερες εφαρμογές React, όχι επειδή δεν διορθώνεται. Δεν είναι εξεζητημένα. Γίνονται καθημερινή πρακτική μηχανικής όταν έχετε δει το κόστος της απουσίας τους.
Η DevLume βοηθά μικρές και μεσαίες ομάδες μηχανικών B2B να διαγιγνώσκουν και να διορθώνουν προβλήματα απόδοσης στην παραγωγή όπως αυτό — συνήθως με στοχευμένη συνεργασία δύο έως τεσσάρων εβδομάδων μαζί με την ομάδα. Αν έχετε έναν πίνακα ελέγχου που μοιάζει αργός και θέλετε να μάθετε αν χρειάζεται δύο ημέρες ή δύο μήνες εργασίας, επικοινωνήστε μαζί μας.

