Core Web Vitals — LCP, INP, CLS მარტივი ენით
შენ ხსნი PageSpeed Insights-ს, წერ შენს დომენს, ელოდები 15 წამს. და იღებ ოთხ ნარინჯისფერ წრესა და ერთ წითელს. ქვემოთ — ასოები LCP, INP, CLS, FCP, TTFB. კიდევ უფრო ქვემოთ ოცდაათი "შესაძლებლობის" სია ინგლისურად: eliminate render-blocking resources, reduce unused JavaScript, properly size images. სქროლავ. არაფერი არ ჩანს ნათელი. ხურავ.
მე გავიარე ეს. და ათეულობით კლიენტთან ერთად გავიარე ეს. ამიტომ მოდი გავარკვიოთ, რა არის აქედან ნამდვილად მნიშვნელოვანი, რა — ხმაური.
მთელი PSI-ის ანგარიშიდან მნიშვნელობა აქვს მხოლოდ სამ ასოს: LCP, INP, CLS. ეს არის Core Web Vitals. Google-მა საჯაროდ დაადასტურა, რომ ისინი შედის რანჟირების სიგნალებში 2021 წლის ივნისიდან. და მას შემდეგ მხოლოდ აძლიერებდა მათ წონას. საერთო SEO-ის 30+ ფაქტორიდან Core Web Vitals — ერთადერთია, რომელსაც Google ოფიციალურად ზომავს რეალური მომხმარებლების ბრაუზერებით.
მეტრიკები განახლდა. 2024 წლის მარტში INP-მა ჩაანაცვლა FID (First Input Delay). FID ითვლიდა მხოლოდ პირველი დაჭერის შეფერხებას. INP უყურებს ვიზიტის ყველა ინტერაქციას და იღებს მათგან ყველაზე ცუდს. უფრო მკაცრია, უფრო რეალისტური, და მისი მოკვლა ახლა უფრო რთულია.
ამ სტატიის მიზანია სამი მეტრიკის ახსნა ადამიანურ ენაზე, კონკრეტული მიზნების მოცემა და კოდში გასაკეთებლის ჩვენება. წყლის გარეშე "დააჩქარეთ თქვენი საიტი"-ს თემაზე.
LCP — Largest Contentful Paint
ეს არის დრო გვერდის ჩატვირთვის დასაწყისიდან იმ მომენტამდე, როცა მომხმარებელი ხედავს ეკრანის ხილულ ნაწილში ყველაზე დიდ ელემენტს. ჩვეულებრივ ეს არის hero-სურათი, ვიდეო ან h1-იანი დიდი ბლოკი. ბრაუზერი თვითონ წყვეტს, რა არის ყველაზე დიდი.
მიზნები:
- < 2.5s — კარგი
- 2.5–4s — საჭიროა მუშაობა
-
4s — ცუდი
ეს 2.5 წამი — შენი დაჭერიდან არ ითვლება. ისინი იმ მომენტიდან ითვლება, როცა ბრაუზერმა მიიღო პირველი ბაიტი. ანუ DNS, TCP, TLS, სერვერის პასუხი, HTML-ის ჩამოტვირთვა, პარსინგი, სურათის ჩამოტვირთვა, რენდერი — ეს ყველაფერი 2500 მილიწამში უნდა ჩაეტიოს.
რა ტეხს ჩვეულებრივ LCP-ს:
მძიმე hero-სურათი. დიზაინერმა გამოგიგზავნა JPEG 4000×3000 2.8 მბ-ზე. შენ ის ოპტიმიზაციის გარეშე ჩაუშვი. ბრაუზერი 4G-ით 2.8 მბ-ს ჩამოტვირთავს, LCP 5 წამს გადასცდება. წყდება next/image-ით priority-სთან ერთად, WebP/AVIF-ით, სწორი ზომებით. Next 15-ზე — <Image src={hero} priority placeholder="blur" /> და ეგ არის.
Render-blocking JS და CSS. <head>-ში დგას სინქრონული <script> ან უზარმაზარი CSS-ფაილი. ბრაუზერი ვერ ხატავს DOM-ს, სანამ მათ არ ჩამოტვირთავს და არ შეასრულებს. გადაწყვეტა: defer/async სკრიპტებზე, კრიტიკული CSS გაიტანე ინლაინად, დანარჩენი — ცალკე ფაილით media="print" ხრიკით ან კრიტიკული CSS-ით.
ნელი TTFB. Time To First Byte. სერვერი 800 მწმ ფიქრობს, სანამ HTML-ს გასცემს. მიზეზები: სერვერული რენდერი ქეშის გარეშე, მძიმე ბაზა, serverless-ფუნქციის ცივი სტარტი, გეოგრაფიულად შორი ჰოსტინგი. გადაწყვეტა: CDN ყველაფრის წინ (Cloudflare, Vercel Edge), ქეშირება გვერდის დონეზე, ISR სტატიკური ნაწილებისთვის.
კრიტიკული რესურსების preload-ის არარსებობა. შრიფტი, რომელიც h1-ში გამოიყენება, იწყებს ჩატვირთვას მხოლოდ CSS-ის პარსინგის შემდეგ. ამ დროს — ცარიელი ადგილია. დადე <link rel="preload" as="font" href="..." crossorigin> მთავარი შრიფტისთვის, და LCP 300–500 მწმ-ით ეცემა.
რა არ უნდა გააკეთო: lazy-load hero-სურათზე. ეს კლასიკური შეცდომაა. შენ გაქვს <img loading="lazy"> სწორედ LCP-სურათზე, და ის ელოდება, ვიდრე scroll-observer გააქტიურებს. LCP ციდან გადადის. hero-ზე — loading="eager" და fetchpriority="high".
კიდევ ერთი ნიუანსი: LCP-ელემენტი შეიძლება შეიცვალოს გვერდის ჩატვირთვის პროცესში. თავიდან ეს h1-ია, შემდეგ დაიხატა hero — გახდა hero. შემდეგ ჩაიტვირთა ვიდეო-ყდა — ის გახდა. ბრაუზერი საბოლოო LCP-ს აფიქსირებს მხოლოდ მაშინ, როცა გვერდი ფონში გადადის ან ხდება პირველი ინტერაქცია. ამიტომ თუ შენ გაქვს ანიმაცია, რომელიც hero-ზე წამში გამოდის — LCP იმ წამიდან ითვლება, და არა საწყისი h1-დან. ჯობია anti-pattern — დაე კრიტიკული ვიზუალი მაშინვე ჩანდეს.
INP — Interaction to Next Paint
ეს არის დრო იმ მომენტიდან, როცა შენ ღილაკზე ტაპი დააწექი, იმ მომენტამდე, როცა ბრაუზერმა ამ ტაპზე რეაქციის შემდეგ შემდეგი კადრი აჩვენა. არა "როცა მოვლენა დამუშავდა", არამედ "როცა მომხმარებელმა შედეგი დაინახა".
მიზნები:
- < 200ms — კარგი
- 200–500ms — საჭიროა მუშაობა
-
500ms — ცუდი
200 მილიწამი — ეს ის ზღვარია, რომლის შემდეგაც ადამიანის ტვინი ინტერფეისს აღიქვამს როგორც "მუხრუჭებს". ნაკლები — რეაგირებს მყისიერად.
INP ითვლის სესიის ყველა ინტერაქციას (კლიკები, ტაპები, კლავიშის დაჭერები) და იღებს მათგან 98-ე პერცენტილს. ანუ საკმარისია ერთი მძიმე კლიკი, რომ მეტრიკა ჩაცვივდეს.
რა ტეხს ჩვეულებრივ INP-ს:
მძიმე JavaScript main thread-ზე. კატალოგში ფილტრზე კლიკი იწყებს დამმუშავებელს, რომელიც 2000 პროდუქტს გადათვლის და მთელ სიას ხელახლა ხატავს. React 600 მწმ-ში აკეთებს reconciliation-ს. თითი უკვე გაუშვა ღილაკი — ეკრანზე არაფერი. გადაწყვეტა: useTransition მძიმე განახლებებისთვის (React 18+), სიების ვირტუალიზაცია, ჩანკებად დაყოფა scheduler.postTask-ით.
Long tasks. ნებისმიერი ამოცანა main thread-ზე 50 მწმ-ზე გრძელი — long task-ია. თუ შენ კლიკის დამმუშავებელში გაქვს მოვლენების ტრეკერი, რომელიც სინქრონულად დადის localStorage-ში, ჯეისონს პარსავს და fetch-ს აგზავნის — ეს ყველაფერი ბლოკავს ნაკადს. გადაწყვეტა: გადაიტანე requestIdleCallback-ზე, ანალიტიკა გაიტანე web worker-ში.
Layout thrashing. კლიკის დამმუშავებელი კითხულობს offsetWidth-ს, შემდეგ ცვლის სტილს, შემდეგ ისევ კითხულობს offsetWidth-ს. ბრაუზერი ყოველ ჯერზე ხელახლა ითვლის layout-ს. რთულ გვერდზე ეს უბრალოდ 100+ მწმ-ს იძლევა. გადაწყვეტა: DOM-ის წაკითხვები და ჩაწერები მოაბათე, გამოიყენე requestAnimationFrame.
მძიმე მესამე მხარის სკრიპტები. Intercom, Hotjar, Google Tag Manager. ისინი თავის დამმუშავებლებს კიდებენ შენების ზემოდან. ყოველი კლიკი გადის გადამხვედრების ჯაჭვით. გადაწყვეტა: ანალიტიკა ჩატვირთე ზარმაცად load-ის შემდეგ, გამოიყენე Partytown სკრიპტების worker-ში გასატანად.
Code-splitting-იც გვეხმარება, მაგრამ არა პირდაპირ — ის ამცირებს არა INP-ს, არამედ პარსირებადი JS-ის რაოდენობას. ირიბად ეს ხსნის main thread-ს.
პრაქტიკიდან: ყველაზე ხშირად INP იჯდება ორ ეკრანზე — ფილტრებიანი სია და ფორმა ავტოშევსებით. ფილტრებზე გვეხმარება შეყვანის დებაუნსი (300 მწმ საკმარისია) პლუს useDeferredValue შედეგების დასახატად. ავტოშევსებაზე — წინა მოთხოვნის გაუქმება ახალი დაჭერისას (AbortController) და ჩამოსაშლელის ვირტუალიზაცია, თუ შედეგი ოცდაათზე მეტია.
CLS — Cumulative Layout Shift
ეს არის გვერდის სიცოცხლის განმავლობაში ყველა ლეიაუთის ცვლის ჯამი. არა დრო, არა მილიწამები — განზომილების გარეშე რიცხვი 0-დან და მეტი. ითვლება როგორც impact × distance: ეკრანის რა ფართობი გადაიწია, viewport-ის რა პროცენტზე გადავიდა.
მიზნები:
- < 0.1 — კარგი
- 0.1–0.25 — საჭიროა მუშაობა
-
0.25 — ცუდი
0.1 პრაქტიკაში — ეს არის, მაგალითად, ეკრანის 30%-ის სურათი, რომელიც viewport-ის სიმაღლის 30%-ით ახტა. ერთი ასეთი ხტომა — და უკვე ზღვარზე ხარ.
CLS — ეს არის იმის შესახებ, რომ "ვაჭერ ღილაკზე, და ამ მომენტში მის ზემოთ გამოდის ბანერი, ვაჭერ ბანერზე". აღიზიანებს ყველას. Google-მა იცის ეს.
რა ტეხს ჩვეულებრივ CLS-ს:
სურათები და iframe-ები მკაფიო ზომების გარეშე. თუ <img>-ს არ აქვს width და height ატრიბუტები (ან მათი CSS-ეკვივალენტები), ბრაუზერმა არ იცის, რა ადგილის რეზერვირება. სურათი ჩაიტვირთება — და მის ქვემოთ მთელ კონტენტს ქვემოთ უბიძგებს. გადაწყვეტა: ყოველთვის მიუთითე width და height (ან aspect-ratio CSS-ში). Next/Image-ში ეს ჩართულია ნაგულისხმევად, თუ ზომებს გადასცემ.
დინამიური ინჯექტები. Cookie-ბანერი, რომელიც ჩატვირთვიდან წამში გამოდის ზემოდან და მთელ გვერდს ქვემოთ წევს. AdSense, რომელიც სტატიის შუაში ბლოკს ჩასვამს. ნებისმიერი A/B-ტესტი, რომელიც DOM-ს ფრენაში ცვლის. გადაწყვეტა: ადგილს წინასწარ აარეზერვირე. Cookie-ბანერი — ფიქსირებული ოვერლეი, და არა block element. რეკლამა — ფიქსირებული სიმაღლის სლოტი.
Font swap size-adjust-ის გარეშე. იტვირთება მორგებული შრიფტი, მანამდე მუშაობს fallback. მათ აქვთ განსხვავებული ხაზის სიმაღლე. როცა შრიფტი ჩაიტვირთა, ტექსტი ხელახლა იწერება, ყველაფერი ხტება. გადაწყვეტა: font-display: swap + size-adjust @font-face-ში, რომ fallback-მა ზუსტად იმდენი ადგილი დაიკავოს. ან font-display: optional, რომელიც არ ცვლის შრიფტს ნელ კავშირზე. Next-ში შესაძლებელია next/font-ით — ის თვითონ ითვლის size-adjust-ს.
ასინქრონული კონტენტი გვერდის ზევით. იტვირთება რეკომენდაციების სია fetch-ით და ჩაშენდება hero-ის ზემოთ. Hero ქვემოთ მიდის. არასოდეს ჩასვა კონტენტი იმის ზემოთ, რაც მომხმარებელს უკვე ჩანს. თუ საჭიროა — ამატებ ქვემოთ, და არა ზემოთ.
Field vs Lab data
ეს ის ადგილია, სადაც ხალხის ნახევარი ცურავს.
PageSpeed Insights გვიჩვენებს ორ ბლოკს. ზემოთ — "Discover what your real users are experiencing" (field data). ქვემოთ — "Diagnose performance issues" (lab data). ეს არის სხვადასხვა რამ.
Lab data — ეს არის Lighthouse-ის ერთი გაშვება იდეალურ პირობებში. ემულირდება Moto G4 ნელ 4G-ზე, გაფართოებების გარეშე, ქეშის გარეშე, სხვა ჩანართების გარეშე. რეპროდუცირებადია, მოსახერხებელია გამართვისთვის, მაგრამ რეალობას არ ასახავს.
Field data — ეს არის CrUX. Chrome User Experience Report. Google აგროვებს ანონიმურ მეტრიკებს Chrome-ის რეალური მომხმარებლებისგან (ვისაც ჩართული აქვს სინქრონიზაცია და ისტორია). აგრეგირებს ბოლო 28 დღის განმავლობაში. თუ შენს საიტზე საკმარისი ხალხი შემოვიდა — Google გვიჩვენებს მათი მეტრიკების 75-ე პერცენტილს.
Google ახარისხებს field-ით. Lab — მხოლოდ მინიშნებაა. შეიძლება გქონდეს Lighthouse-ში 100/100 და ამავდროულად წითელი CrUX, რადგან რეალური მომხმარებლები მოდიან 2019 წლის ნელი Android-ფლაგმანებიდან მეტროთი.
სად ვუყუროთ field-ს:
- PageSpeed Insights — ყველაზე მარტივი გზა, მთავარი გვერდი და ცალკეული URL-ები.
- Search Console → Core Web Vitals report — იძლევა დაყოფას URL-ების ჯგუფებად: "ძიება", "პროდუქტის ბარათები", "სტატიები". ჩანს, სად ჯდება ზუსტად.
- PSI API — ავტომატიზაციისთვის. ატარებ სკრიპტით კვირაში ერთხელ მნიშვნელოვანი გვერდების სიაზე, ალაგებ ცხრილში, აშენებ დინამიკას.
- CrUX BigQuery dataset — მათთვის, ვინც SQL-ს იცის. შესაძლებელია სრული განაწილებების ამოღება, და არა მხოლოდ პერცენტილების.
ერთი მნიშვნელოვანი მომენტი. თუ საიტს ცოტა ტრაფიკი აქვს — CrUX field data-ს არ აჩვენებს. უბრალოდ იტყვის "მონაცემები საკმარისი არ არის". ამ შემთხვევაში Google რანჟირებისთვის იყენებს origin-ის დონის data-ს (მთელი დომენისთვის) ან გადადის lab-ზე. ამიტომ ახალგაზრდა საიტზეც კი აზრი აქვს საუკეთესო Lighthouse-ის გამოგდებას — ეს შენი დაზღვევაა, სანამ CrUX დაგროვდება.
რა არ ეხმარება
ნივთების სია, რომელსაც ხშირად გირჩევენ, მაგრამ რომელიც Core Web Vitals-ზე გავლენას არ ახდენს ან უარყოფითად ახდენს.
CSS-ის მინიფიკაცია ისედაც მსუბუქ საიტზე. შენ გაქვს 18 კბ CSS, gzip ამას ხრავს 4 კბ-მდე. PSI წერს "minify CSS, save 1.2 KB". ეს 1.2 კბ არაფერს არ მოგცემს. დროს ფუჭად ხარჯავ.
Lazy load ყველაფერზე ზედიზედ. loading="lazy" ყველა სურათზე — ყველაზე გავრცელებული შეცდომაა Chrome-ში ნატივ lazy-ის შემოღების შემდეგ. hero-ის ჩათვლით. შარფის ლოგოს ჩათვლით. შედეგად fold-ის ზემოთ მყოფი სურათები ელოდებიან intersection observer-ს, იმის ნაცვლად, რომ მაშინვე ჩაიტვირთონ. LCP იზრდება. Lazy მხოლოდ იმაზე იდება, რაც fold-ის ქვემოთაა.
აგრესიული preload ყველაფერზე, რაც მოძრაობს. წავიკითხე სტატია "preload critical resources"-ის შესახებ, დავკიდე <link rel="preload"> ათ ფაილზე. ბრაუზერი მათ HTML-ზე უფრო მაღალი პრიორიტეტით ჩამოტვირთავს. ქსელი დატვირთულია. LCP-სურათი თავის რიგს ელოდება. Preload — ეს მახვილი ინსტრუმენტია. მაქსიმუმ ორი-სამი კრიტიკული რესურსი.
Service Worker-ი "ყველაფერზე" ქეშით. ქეშირება ჭკვიანია, მაგრამ თუ შენ არ გესმის, რას ქეშავ — მომხმარებელი საიტის ძველ ვერსიას ხედავს. Core Web Vitals-ს ეს საერთოდ არ ეხება, პრობლემებს კი მატებს.
შედეგი
თუ შენ შეზღუდული დრო გაქვს — პრიორიტეტიზაცია ასეთია.
LCP პირველ რიგში. ნელი გვერდი — მომხმარებლები გარბიან, სანამ რამეს დაინახავენ. ეს არტყამს როგორც რანჟირებას, ისე კონვერსიას პირდაპირ. Hero-სურათი next/image priority-ით, შრიფტების preload, CDN ყველაფრის წინ. ეს იძლევა 80% შედეგს.
CLS მეორედ. მხტუნავი ლეიაუთი აღიზიანებს და კლავს ნდობას. განსაკუთრებით მობილურზე. ზომები მედიაზე, font-display size-adjust-ით, ნუ ჩასვამ კონტენტს უკვე ხილულის ზემოთ. ეს გასწორებები სწრაფად კეთდება და არ ბრუნდება.
INP მესამედ. ეს ყველაზე დახვეწილია და მეტ მუშაობას მოითხოვს — პროფილირება, მძიმე დამმუშავებლების გარკვევა, რეფაქტორინგი. მაგრამ LCP-ისა და CLS-ის წესრიგში ყოფნის გარეშე INP-ზე გადასვლა ნაადრევია.
მეტრიკებს უყურე field-ში — Search Console და PSI. Lab გამოიყენე მხოლოდ როგორც ქვიშათამაში დეპლოის წინ ცვლილებების შესამოწმებლად. და გახსოვდეს: ერთი გაშვება საკმარისი არ არის. CrUX დღეში ერთხელ ნახლდება, აგრეგირებს 28 დღის განმავლობაში. ცვლილებების შემდეგ მინიმუმ ორ კვირას დაელოდე, სანამ დასკვნებს გააკეთებ.