30 վայրկյանում
Յուրաքանչյուր վեբ էջ կառուցված է երեք շերտից։ HTML-ը նկարագրում է, թե ինչ է կոնտենտը (վերնագիր, կոճակ, ձև), CSS-ը որոշում է, թե ինչպես է այն երևում և ինչպես է հարմարվում յուրաքանչյուր էկրանին, իսկ JavaScript-ը ավելացնում է վարքագիծ, օրինակ՝ click-երին արձագանքել կամ տվյալներ բեռնել։ React-ի և Next.js-ի պես framework-ները նստած են այս երեքի վրա. դրանք լավ հասկանալը քեզ ավելի արագ է դարձնում, իսկ քո կայքերը՝ ավելի հասանելի ցանկացած framework-ում։
Ինչու եմ օգտագործում
- Յուրաքանչյուր ինտերֆեյս, անկախ framework-ից, կառուցում եմ semantic HTML-ի և մաքուր CSS-ի սկզբունքների վրա, որովհետև հենց դա են իրականում հասկանում բրաուզերները, որոնողական համակարգերը և screen reader-ները։
- Նկատում եմ, որ «framework-ի բագերի» մեծ մասը, որոնք debug եմ անում, իրականում HTML-ի, CSS-ի կամ DOM-ի հարցեր են՝ բացակայող label, collapse եղող margin, event, որը bubble է անում այնտեղ, որտեղ ոչ ոք չէր սպասում։
- Layout-ները նախագծում եմ mobile-first՝ Flexbox-ով, Grid-ով և design token-ներով, և ստուգում եմ դրանք հեռախոսի, պլանշետի և desktop-ի լայնություններում՝ նախքան էջն ավարտված համարելը։
- Accessibility-ն համարում եմ որակի մաս, ոչ թե հավելում. semantic markup-ը ոչինչ չարժե և օգնում է յուրաքանչյուր օգտատիրոջ։
Ինչպես է աշխատում
01Semantic HTML և accessibility-ի հիմունքներ
HTML տարրերը իմաստ են կրում։ <button>-ը focus է ստանում, աշխատում է Enter-ով և Space-ով և screen reader-ների կողմից հայտարարվում է որպես կոճակ. click handler-ով <div>-ը սրանցից ոչ մեկը չէ։ Օգտագործիր landmark-ներ (<header>, <nav>, <main>, <footer>), մեկ էջում մեկ <h1>՝ հերթականությամբ վերնագրերով, <a> նավիգացիայի և <button> գործողությունների համար, իսկ ցուցակների համար՝ ցուցակներ։ Ձևի յուրաքանչյուր դաշտի պետք է տեսանելի <label>՝ կապված for-ով և id-ով։ Նկարներին պետք է alt տեքստ, որը նկարագրում է դրանց նպատակը, կամ alt="", եթե դրանք դեկորատիվ են։ Համոզվիր, որ ամեն ինչ աշխատում է միայն ստեղնաշարով, և focus outline-ը տեսանելի է։ Նպատակ դիր WCAG 2.2 AA մակարդակը, որը ներառում է սովորական տեքստի համար առնվազն 4.5:1 կոնտրաստ։ ARIA-ն օգտագործիր միայն երբ ոչ մի native տարր չի համապատասխանում. ARIA-ի առաջին կանոնը native HTML-ը նախընտրելն է։
<header>
<nav aria-label="Main">
<a href="/">Home</a>
<a href="/shop" aria-current="page">Shop</a>
</nav>
</header>
<main>
<h1>Running shoes</h1>
<img src="/img/shoe.webp" alt="Blue running shoe, side view" width="640" height="480" />
<form action="/subscribe" method="post">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required />
<button type="submit">Subscribe</button>
</form>
</main>
02Box model և cascade
Յուրաքանչյուր տարր ուղղանկյուն box է չորս շերտով՝ content, padding (տարածություն border-ի ներսում), border և margin (տարածություն դրսում)։ Լռելյայն width-ը կիրառվում է միայն content-ի վրա, ուստի padding ավելացնելը box-ը դարձնում է ավելի լայն, քան խնդրել ես։ Ամեն ինչի վրա box-sizing: border-box դնելը ստիպում է width-ին ներառել padding-ը և border-ը, ինչը շատ ավելի ինտուիտիվ է։ Block-երի միջև ուղղահայաց margin-ները կարող են collapse լինել մեկի մեջ, ինչը դասական անակնկալ է. Flexbox-ում և Grid-ում gap-ը դրանից խուսափում է։ Երբ մի քանի կանոն թիրախավորում է նույն տարրը, cascade-ը որոշում է, թե ով է հաղթում՝ importance, հետո specificity (id-ն հաղթում է class-ին, class-ը՝ element-ին), հետո սորսի հերթականությունը։ Specificity-ն պահիր ցածր և հարթ, հիմնականում մեկ class-ով, և խուսափիր !important-ից։ Ժամանակակից CSS-ը նաև առաջարկում է custom property-ներ (փոփոխականներ) design token-ների համար, cascade layer-ներ (@layer)՝ ոճերի ամբողջական խմբեր դասավորելու համար, և native nesting։
:root {
--space-2: 0.5rem;
--space-4: 1rem;
--radius: 12px;
--color-surface: #ffffff;
}
*, *::before, *::after {
box-sizing: border-box;
}
.card {
padding: var(--space-4);
border: 1px solid #dddddd;
border-radius: var(--radius);
background: var(--color-surface);
}
.card:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}
03Flexbox, թե Grid
Երկուսն էլ layout համակարգեր են, և ժամանակակից կայքերն օգտագործում են երկուսն էլ։ Flexbox-ը միաչափ է. այն տարրերը դասավորում է մեկ տողում կամ սյունակում և գերազանց է տարածություն բաշխելու և հավասարեցնելու մեջ, օրինակ՝ navbar, պատկերակով կոճակ կամ tag-երի տող, որը wrap է անում։ Մտածում ես content-first. տարրերն իրենք են որոշում իրենց չափը, իսկ Flexbox-ը բաշխում է մնացած տարածությունը։ Grid-ը երկչափ է. սահմանում ես տողեր և սյունակներ, և տարրերը տեղադրվում են այդ կառուցվածքում, ինչը հարմար է էջերի layout-ների, քարտերի պատկերասրահների և dashboard-ների համար։ Մտածում ես layout-first։ Grid-ի օգտակար pattern է repeat(auto-fill, minmax(240px, 1fr))-ը, որը ստեղծում է այնքան սյունակ, որքան տեղավորվում է, ընդհանրապես առանց media query-ների։ Երկուսն էլ աջակցում են gap՝ հետևողական տարածությունների համար։ Գործնական կանոն. Grid՝ ընդհանուր կառուցվածքի համար, Flexbox՝ դրա ներսի կոմպոնենտների համար։
/* Flexbox: a toolbar with items on both ends */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
/* Grid: responsive cards without media queries */
.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.5rem;
}
04Responsive դիզայն
Responsive կայքն աշխատում է ցանկացած էկրանի վրա՝ 360 պիքսելանոց հեռախոսից մինչև լայն մոնիտոր։ Սկսիր viewport meta tag-ից, այլապես մոբայլ բրաուզերները կցուցադրեն փոքրացված desktop էջ։ Նախագծիր mobile-first. հիմնական ոճերը գրիր փոքր էկրանների համար և ավելացրու media query-ներ մեծերի համար՝ breakpoint-ներն ընտրելով այնտեղ, որտեղ քո կոնտենտն է «կոտրվում», ոչ թե կոնկրետ սարքերի համար։ Օգտագործիր հարաբերական միավորներ (rem՝ տառատեսակի և տարածությունների համար, % և fr՝ լայնությունների համար) և clamp()՝ սահուն տառաչափերի համար։ Container query-ները, որոնք այժմ աջակցվում են բոլոր ժամանակակից բրաուզերներում, թույլ են տալիս կոմպոնենտին հարմարվել իր ծնողի լայնությանը՝ ամբողջ պատուհանի փոխարեն, ինչը իդեալական է վերաօգտագործվող քարտերի համար։ Նկարների համար օգտագործիր srcset և sizes, որպեսզի հեռախոսները փոքր ֆայլեր ներբեռնեն, դիր width և height՝ layout shift-ը կանխելու համար, և lazy-load արա fold-ից ներքև գտնվող նկարները։ Թեստավորիր իրական սարքերի վրա և բրաուզերի device toolbar-ով։
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3rem); }
.layout { display: grid; gap: 1rem; }
@media (min-width: 768px) {
.layout { grid-template-columns: 240px 1fr; }
}
.card-wrap { container-type: inline-size; }
@container (min-width: 420px) {
.card { display: flex; gap: 1rem; }
}
</style>
<img src="/img/hero-800.webp" srcset="/img/hero-400.webp 400w, /img/hero-800.webp 800w, /img/hero-1600.webp 1600w"
sizes="(min-width: 768px) 50vw, 100vw" width="800" height="450" alt="Product on a desk" loading="lazy" />
05DOM և event-ներ
Երբ բրաուզերը parse է անում HTML-ը, կառուցում է DOM-ը (Document Object Model)՝ օբյեկտների ծառ, որը JavaScript-ը կարող է կարդալ և փոխել։ Տարրերը գտնում ես querySelector-ով, փոխում ես textContent-ի, classList-ի և dataset-ի պես property-ների միջոցով, և օգտատիրոջն արձագանքում ես addEventListener-ով։ Event-ները ճանապարհորդում են. click-ը սկսվում է թիրախ տարրից և bubble է անում վեր՝ նրա նախնիների միջով։ Սա հնարավոր է դարձնում event delegation. մեկ listener ծնողի վրա, որը մշակում է իր բոլոր երեխաների click-երը, այդ թվում հետո ավելացվածների։ event.preventDefault()-ը կանգնեցնում է բրաուզերի լռելյայն գործողությունը, օրինակ՝ ձևի submit-ը. event.target-ը ասում է, թե իրականում ինչի վրա են սեղմել։ Օգտատիրոջ տվյալների համար օգտագործիր textContent, ոչ թե innerHTML՝ cross-site scripting-ից (XSS) խուսափելու համար։ Script-երը բեռնիր defer-ով կամ type="module"-ով, որպեսզի դրանք չարգելափակեն էջի rendering-ը։
<ul id="cart">
<li>Coffee beans <button data-id="101" class="remove">Remove</button></li>
<li>Filter papers <button data-id="102" class="remove">Remove</button></li>
</ul>
<p id="status" role="status"></p>
<script type="module">
const cart = document.querySelector("#cart");
const status = document.querySelector("#status");
// One listener for every current and future button (delegation)
cart.addEventListener("click", (event) => {
const button = event.target.closest("button.remove");
if (!button) return;
button.closest("li").remove();
status.textContent = `Item ${button.dataset.id} removed`; // safe: no innerHTML
});
</script>
06Տվյալների բեռնում fetch-ով
fetch()-ը HTTP հարցումներ անելու բրաուզերի ներկառուցված միջոցն է, և այն հասանելի է նաև Node.js-ում։ Այն վերադարձնում է Response-ի promise։ Երկու մանրուք մարդկանց հաճախ սայթաքեցնում է։ Առաջին՝ fetch-ը reject է անում միայն ցանցի ձախողման դեպքում. 404-ը կամ 500-ը դեռ resolve են լինում, ուստի միշտ ստուգիր response.ok-ը կամ response.status-ը։ Երկրորդ՝ body-ն կարդացվում է առանձին՝ response.json()-ով կամ response.text()-ով, որը նույնպես ասինխրոն է։ JSON ուղարկիր method-ով, Content-Type header-ով և body-ի համար JSON.stringify-ով։ Օգտագործիր AbortController՝ հարցումը չեղարկելու համար, երբ օգտատերը հեռանում է էջից կամ նոր որոնում է մուտքագրում, և AbortSignal.timeout()՝ անվերջ սպասելուց խուսափելու համար։ UI-ում ցույց տուր loading և error վիճակները և անջատիր submit կոճակը, քանի դեռ հարցումն ընթացքի մեջ է, որպեսզի օգտատերերը չկարողանան այն երկու անգամ ուղարկել։ Cross-origin հարցումները կարգավորվում են CORS-ով, որը պետք է թույլատրի սերվերը, ոչ թե բրաուզերի կոդը։
async function searchProducts(query, signal) {
const res = await fetch(
`https://api.example.com/products?q=${encodeURIComponent(query)}`,
{ headers: { Accept: "application/json" }, signal }
);
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
}
let controller;
document.querySelector("#search").addEventListener("input", async (e) => {
controller?.abort(); // cancel the previous, now outdated request
controller = new AbortController();
try {
const results = await searchProducts(e.target.value, controller.signal);
console.log(results);
} catch (err) {
if (err.name !== "AbortError") console.error(err);
}
});
Օգտագործեք, երբ
- Միշտ. յուրաքանչյուր վեբ framework վերջում render է անում HTML, CSS և JavaScript։
- Պարզ կայքեր, landing էջեր և email-ի նման կոնտենտ, որտեղ մաքուր HTML-ն ու CSS-ը արագ են և build քայլ չեն պահանջում։
- Progressive enhancement. ձև, որն աշխատում է առանց JavaScript-ի և դրանով ավելի հաճելի է դառնում։
- Framework հավելվածների debug, որտեղ բրաուզերի Elements, Network և Console պանելները ցույց են տալիս, թե իրականում ինչ է կատարվում։
Մի՛ օգտագործեք, երբ
- Մեծ ինտերակտիվ հավելվածներ՝ շատ ընդհանուր վիճակով, որտեղ ձեռքով գրված DOM կոդը դժվար է սպասարկել, և կոմպոնենտային framework-ը (React, Vue, Svelte) արդարացնում է իրեն։
- Բարդ widget-ները (date picker-ներ, combobox-ներ, modal-ներ) զրոյից վերագրելը, երբ արդեն կա լավ թեստավորված, հասանելի կոմպոնենտ։
Այլընտրանքներ
- Component frameworks (React, Vue, Svelte)
UI-ը նկարագրում են որպես վիճակով կոմպոնենտներ. դրանք միևնույն է արտադրում են HTML, CSS և DOM թարմացումներ։
- Utility CSS (Tailwind)
Ոճավորում փոքր class-ներով հենց markup-ում՝ առանձին stylesheet-ների փոխարեն. արագ է, բայց միևնույն է պետք է հասկանաս box model-ը, Flexbox-ը և Grid-ը։
- htmx and server-rendered HTML
HTML ուղարկիր սերվերից և փոխարինիր fragment-ներ՝ շատ քիչ JavaScript-ով. լավ է ադմին և CRUD հավելվածների համար։
Լավագույն պրակտիկաներ
- Նախ օգտագործիր native տարրեր՝ <button> գործողությունների համար, <a> նավիգացիայի համար, <label> յուրաքանչյուր input-ի համար։
- Գրիր mobile-first CSS design token-ներով (custom property-ներ)՝ hard-code արված գույների և չափերի փոխարեն։
- Օգտագործիր Grid էջի կառուցվածքի համար, Flexbox՝ կոմպոնենտների համար, և gap՝ տարածությունների համար։
- Նկարներին տուր width, height, alt տեքստ և responsive srcset. fold-ից ներքև նկարները lazy-load արա։
- Focus-ը պահիր տեսանելի, թեստավորիր միայն ստեղնաշարով և ստուգիր գույների կոնտրաստը WCAG 2.2 AA-ի դեմ։
- Յուրաքանչյուր fetch-ից հետո ստուգիր response.ok-ը, ցույց տուր loading և error վիճակները և չեղարկիր հնացած հարցումները։
- Անիմացիաների համար հարգիր prefers-reduced-motion-ը։
Տարածված սխալներ
- Սեղմվող <div>-եր կոճակների փոխարեն, որոնցից ստեղնաշարի և screen reader-ի օգտատերերը չեն կարող օգտվել։
- Input-ներ առանց label-ների՝ հույս դնելով placeholder տեքստի վրա, որն անհետանում է մուտքագրելիս։
- Viewport meta tag-ը մոռանալը, ինչի պատճառով մոբայլ կայքը ցուցադրվում է փոքրացված։
- Ենթադրելը, որ fetch-ը HTTP սխալների դեպքում exception է նետում, և 500 պատասխանից հետո «հաջողություն» ցույց տալը։
- Օգտատիրոջ մուտքն innerHTML-ով տեղադրելը և XSS-ի համար դուռ բացելը։
Խորը վերլուծություն
07Rendering pipeline-ը և Core Web Vitals-ը
Էջը նկարելու համար բրաուզերը HTML-ը parse է անում DOM-ի, CSS-ը՝ CSSOM-ի, դրանք միավորում է render tree-ի մեջ, հաշվարկում է layout-ը (յուրաքանչյուր box-ի չափն ու դիրքը), պիքսելները նկարում է շերտերում (paint) և վերջում այդ շերտերը GPU-ի վրա համադրում է (composite)։ head-ում CSS-ն արգելափակում է rendering-ը, իսկ առանց defer-ի դասական script-ները արգելափակում են parsing-ը, ահա թե ինչու կրիտիկական CSS-ը պետք է փոքր լինի, իսկ script-ները՝ defer արված։ width-ի կամ top-ի նման layout property-ի փոփոխությունը նորից layout է պարտադրում, երբեմն ամբողջ էջի համար. transform-ի և opacity-ի անիմացիան սովորաբար պահանջում է միայն compositing, որը շատ ավելի էժան է։ Style-եր գրելուց անմիջապես հետո offsetHeight-ի նման layout արժեք կարդալը սինխրոն layout է պարտադրում, իսկ դա ցիկլում անելը (layout thrashing) սառեցնում է էջը։ Google-ի Core Web Vitals-ը սա վերածում է չափելի թիրախների՝ իրական օգտատերերի 75-րդ percentile-ում. Largest Contentful Paint-ը՝ 2.5 վայրկյանից պակաս, Interaction to Next Paint-ը (որը 2024-ի մարտին փոխարինեց First Input Delay-ին)՝ 200 միլիվայրկյանից պակաս, իսկ Cumulative Layout Shift-ը՝ 0.1-ից պակաս։
// Layout thrashing: read, write, read, write... forces layout each time
for (const card of cards) {
card.style.height = card.offsetWidth * 0.75 + "px";
}
// Better: batch all reads, then all writes
const widths = cards.map((card) => card.offsetWidth);
cards.forEach((card, i) => {
card.style.height = widths[i] * 0.75 + "px";
});
// Best for this case: let CSS do it, no JavaScript at all
// .card { aspect-ratio: 4 / 3; }
// Yield to the browser during long work so input stays responsive (INP)
await new Promise((resolve) => setTimeout(resolve, 0));
08Ժամանակակից CSS ճարտարապետություն՝ layer-ներ, :where(), :has() և specificity-ի վերահսկում
Մեծ stylesheet-երը ձախողվում են specificity-ի պատերազմների պատճառով. ամեն ուղղում պահանջում է նախորդից ուժեղ selector, մինչև !important-ը հայտնվում է ամենուր։ Ժամանակակից CSS-ը գործիքներ է տալիս սա գիտակցաբար վերահսկելու համար։ Cascade layer-ները (@layer reset, base, components, utilities) առաջնահերթությունը որոշում են ըստ layer-ների հերթականության՝ նախքան specificity-ն ընդհանրապես համեմատվի, այնպես որ ավելի ուշ layer-ում պարզ utility class-ը հաղթում է ավելի վաղ layer-ի բարդ component selector-ին։ :where()-ը համընկնում է :is()-ի պես, բայց զրո specificity է ավելացնում, ինչը կատարյալ է reset-ների և գրադարանային style-երի համար, որոնք պետք է հեշտ override լինեն։ :has()-ը երկար սպասված parent selector-ն է. style տուր նկար պարունակող card-ին կամ ձևի դաշտին, որի input-ը անվավեր է՝ առանց JavaScript-ի։ Custom property-ները ժառանգվում են ծառի միջով և կարող են փոխվել runtime-ում, այնպես որ theme-ը պարզապես արժեքների այլ հավաքածու է parent էլեմենտի վրա։ Միասին դրանք թույլ են տալիս գրեթե ամեն selector պահել մեկ class-ի մակարդակում և միևնույն ժամանակ ունենալ կանխատեսելի override-ներ։
@layer reset, base, components, utilities;
@layer reset {
:where(ul, ol) { margin: 0; padding: 0; list-style: none; }
}
@layer components {
.card { padding: var(--space-4); border-radius: var(--radius); }
.card:has(> img) { padding-top: 0; } /* parent selector */
.field:has(input:user-invalid) { --border: #b91c1c; }
}
@layer utilities {
.p-0 { padding: 0; } /* wins over .card because of layer order */
}
[data-theme="dark"] { --color-surface: #111318; --color-text: #e8e8ea; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition: none !important; }
}
09Պլատֆորմի native UI՝ dialog, popover, inert և focus-ի կառավարում
Modal-ները, մենյուները և tooltip-ները հասանելի դարձնելու համար նախկինում հարյուրավոր տող JavaScript էր պետք։ Հիմա պլատֆորմը դրա մեծ մասն ինքն է անում։ showModal()-ով բացված <dialog> էլեմենտը պահում է focus-ը իր ներսում, էջի մնացած մասը դարձնում է inert, փակվում է Escape-ով և նկարվում է ամեն ինչից վեր՝ top layer-ում։ Popover API-ը (popover ատրիբուտը՝ կոճակի վրա popovertarget-ով, Baseline 2024-ից) ստեղծում է light-dismiss popup-ներ, օրինակ՝ մենյուներ, առանց որևէ script-ի։ inert ատրիբուտը ամբողջ ենթածառը հանում է focus-ից և accessibility tree-ից, ինչն օգտակար է off-canvas նավիգացիայի համար։ Focus-ի կառավարումը դեռ քո պատասխանատվությունն է. երբ dialog-ը փակվում է, focus-ը պետք է վերադառնա այն կոճակին, որը բացել էր այն (dialog-ը դա անում է քո փոխարեն), իսկ client-side նավիգացիայից հետո focus-ը տեղափոխիր նոր էջի վերնագրին, որպեսզի screen reader-ի օգտատերերն իմանան, որ բովանդակությունը փոխվել է։ Ասինխրոն արդյունքները հայտարարիր live region-ի միջոցով (role="status" կամ aria-live)՝ միայն տեսողական փոփոխություններին հույս դնելու փոխարեն։
<button popovertarget="account-menu">Account</button>
<div id="account-menu" popover>
<a href="/orders">Orders</a>
<a href="/settings">Settings</a>
</div>
<button id="open-delete">Delete address</button>
<dialog id="confirm-delete" aria-labelledby="confirm-title">
<h2 id="confirm-title">Delete this address?</h2>
<form method="dialog">
<button value="cancel">Cancel</button>
<button value="confirm">Delete</button>
</form>
</dialog>
<script type="module">
const dialog = document.querySelector("#confirm-delete");
document.querySelector("#open-delete").addEventListener("click", () => dialog.showModal());
dialog.addEventListener("close", () => console.log(dialog.returnValue));
</script>
10Front-end-ի անվտանգություն՝ XSS, CSP և երրորդ կողմի ապահով script-ներ
Cross-site scripting-ը (XSS) տեղի է ունենում, երբ հարձակվողի վերահսկած տեքստը մեկնաբանվում է որպես կոդ. տեղադրված innerHTML-ով, href-ում դրված որպես javascript:, կամ կցված inline event handler-ին։ Լռելյայն escaping-ը (textContent, framework-ի templating) կանխում է դրա մեծ մասը. երբ իսկապես պետք է օգտատերերից եկած HTML նկարել, օրինակ՝ rich-text մեկնաբանություններ, sanitize արա այն DOMPurify-ի նման սպասարկվող գրադարանով։ Content Security Policy-ն պաշտպանության երկրորդ գիծն է. HTTP header, որը բրաուզերին ասում է, թե script-ների որ աղբյուրներն են թույլատրված, այնպես որ ներարկված script-ները չեն աշխատում։ Խիստ policy-ն օգտագործում է ամեն request-ի համար nonce կամ hash-եր՝ ամբողջ domain-ներ whitelist անելու փոխարեն։ Trusted Types-ը, որտեղ աջակցվում է, ավելի հեռու է գնում՝ ստիպելով բոլոր վտանգավոր DOM sink-երին անցնել review արված policy-ի միջով։ Քո ավելացրած յուրաքանչյուր երրորդ կողմի script աշխատում է քո էջի լիարժեք հասանելիությամբ, ներառյալ ձևերի դաշտերը, ուստի բեռնիր հնարավորինս քիչ, ֆիքսիր տարբերակները, CDN-ներից ստատիկ ֆայլերի համար օգտագործիր Subresource Integrity և երբեք analytics մի դիր վճարման էջերում։
# HTTP response header (one line in practice), nonce generated per request
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123' 'strict-dynamic';
style-src 'self';
img-src 'self' data: https://cdn.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
<!-- Static library from a CDN, verified by hash -->
<script src="https://cdn.example.com/[email protected]/lib.min.js"
integrity="sha384-BASE64_HASH_OF_THE_FILE" crossorigin="anonymous" defer></script>
Իրական իրավիճակներ
Էջը բեռնվելիս ցատկում է, և օգտատերերը սխալ կոճակն են սեղմում
Խնդիր
Մոբայլում ապրանքի էջը բեռնվելիս մի քանի անգամ տեղաշարժվում է. հայտնվում է banner, font-երը փոխվում են, իսկ նկարները բովանդակությունը ներքև են հրում։ Օգտատերերը բողոքում են պատահական սեղմումներից, և field data-ում CLS-ը 0.32 է։
Ինչպես լուծել
Նախ գտիր տեղաշարժվող էլեմենտները. Chrome DevTools-ի Performance panel-ը նշում է layout shift-երը, իսկ Web Vitals extension-ը ցույց է տալիս, թե որ node-երն են շարժվել։ Հետո տեղ պահիր այն ամենի համար, ինչ ուշ է գալիս։ Յուրաքանչյուր նկարի տուր width և height կամ CSS-ում aspect-ratio։ Banner-ի slot-ի համար պահիր ֆիքսված min-height կամ նկարիր այն fold-ից ներքև։ Օգտագործիր font-display: swap՝ չափով ճշգրտված fallback font-ով (size-adjust կամ framework-ի font loader), որպեսզի տեքստը reflow չանի։ Երբեք բովանդակություն մի ավելացրու առկա բովանդակությունից վեր, եթե դա օգտատիրոջ գործողության պատասխան չէ։ Deploy արա, հետո հետևիր 75-րդ percentile-ի CLS-ին իրական օգտատերերի տվյալներում, որովհետև lab թեստերը բաց են թողնում գովազդից և դանդաղ ցանցերից առաջացած shift-երը։
Accessibility audit-ը տապալում է checkout-ը
Խնդիր
Հաճախորդի audit-ը հաղորդում է, որ checkout-ը հնարավոր չէ ավարտել ստեղնաշարով, սխալի հաղորդագրությունները չեն հայտարարվում, և մի քանի կոճակի կոնտրաստը չի անցնում։ Իրավական deadline-ը մեկ ամսից է։
Ինչպես լուծել
Ուղղիր ըստ ազդեցության՝ սկսելով արգելափակողներից։ Սեղմվող div-երը փոխարինիր իրական button-ներով և link-երով, համոզվիր, որ ամեն քայլ աշխատում է Tab-ով, Enter-ով, Space-ով և Escape-ով, և պահիր տեսանելի :focus-visible outline։ Յուրաքանչյուր input կապիր label-ի հետ, սխալները նշիր aria-invalid-ով և հաղորդագրությունը միացրու aria-describedby-ով. submit-ի ժամանակ focus-ը տեղափոխիր սխալների ամփոփագրին՝ դաշտերի link-երով։ «Card declined»-ի նման ասինխրոն արդյունքները հայտարարիր role="alert" region-ում։ Գույների token-ները ուղղիր մեկ անգամ, որպեսզի յուրաքանչյուր կոճակ անցնի 4.5:1-ը։ Ստուգիր axe DevTools-ով, հետո ձեռքով թեստավորիր screen reader-ով, օրինակ՝ VoiceOver-ով կամ NVDA-ով, որովհետև ավտոմատ գործիքները բռնում են խնդիրների միայն մի մասը։
Մարքեթինգային script-ը դանդաղեցնում է յուրաքանչյուր ինտերակցիա
Խնդիր
Chat-ի, heatmap-ի և երկու tracking script-ի ավելացումից հետո կայքը «ծանր» է զգացվում։ Field data-ում INP-ն բարձրացել է մինչև 450 ms, իսկ main thread-ը բեռնումից հետո վայրկյաններով զբաղված է։
Ինչպես լուծել
Profile արա Chrome-ի Performance panel-ում CPU throttling-ով և ակտիվությունը խմբավորիր ըստ երրորդ կողմի՝ տեսնելու, թե որ script-ին են պատկանում long task-երը։ Հեռացրու script-ները, որոնք ոչ ոք չի օգտագործում. սովորաբար այդպիսին մեկը կա։ Մնացածը բեռնիր defer-ով կամ load event-ից հետո, իսկ chat widget-ը բեռնիր միայն այն ժամանակ, երբ օգտատերը սեղմում է «Chat» կոճակը (facade)։ Tag manager-ի tag-երը տեղափոխիր consent-ի հետևում, ինչը, միևնույն է, պարտադիր է։ Քո կոդում երկար handler-ները բաժանիր փոքր մասերի, դրանց միջև main thread-ին հերթ զիջիր (yield) և ծանր աշխատանքը, օրինակ՝ մեծ ցուցակների ֆիլտրումը, հեռու պահիր input handler-ներից։ Մի քանի օր անց նորից ստուգիր INP-ն real-user monitoring-ում, որովհետև այն field metric է։
Արագ ուղեցույց
- Modal՝ focus trap-ով և Escape-ով
dialog.showModal() // <form method="dialog"> closes it- Մենյու առանց JavaScript-ի
<button popovertarget="menu">Menu</button> <div id="menu" popover>...</div>- Թաքցնել հատվածը focus-ից և screen reader-ներից
<aside inert>...</aside>- Cascade-ի հերթականության սահմանում
@layer reset, base, components, utilities;- Zero-specificity reset
:where(ul, ol) { list-style: none; }- Parent-ին style տալ ըստ իր child-ի
.card:has(> img) { padding-top: 0; }- Տեղ պահել մեդիայի համար
img, video { aspect-ratio: 16 / 9; width: 100%; height: auto; }- Հայտարարել ասինխրոն թարմացումները
<p role="status" aria-live="polite"></p>- Կրճատված շարժում
@media (prefers-reduced-motion: reduce) { ... }- Հիմնական CSP header
Content-Security-Policy: default-src 'self'; frame-ancestors 'none'
Իմ իրական նախագծերում
- Գործակալության մարքեթինգային կայքը վերակառուցեցի զրոյից՝ ներառյալ ինտերակտիվ էֆեկտներ, dark mode, sitemap և SEO redirect-ներ, հենց այս հիմունքների վրա։
- Կենդանիների ապրանքների խանութում ամբողջ թիմի համար գրի առա design-system-ի կանոնները. միայն design token-ներ, ոչ մի raw գույն կամ պիքսելային արժեք և յոթ սահմանված breakpoint։
- Էջերը ստուգում եմ հեռախոսի, պլանշետի և desktop-ի լայնություններում (390, 768 և 1920 պիքսել)՝ ավտոմատ browser-verification քայլով, նախքան աշխատանքն ընդունվելը։
- Մեր ներքին փաթեթները ներառում են design inspector, որը դիզայներներին թույլ է տալիս CSS փոփոխականները կարգավորել live բրաուզերում և պահպանել դրանք հետ՝ սորսում։
Հարցեր և պատասխաններ
Q1Ի՞նչ է semantic HTML-ը և ինչո՞ւ է այն կարևոր։
Semantic HTML նշանակում է տարրերն ընտրել իրենց իմաստով, ոչ թե տեսքով՝ <nav> նավիգացիայի համար, <button> գործողությունների համար, <h2> բաժնի վերնագրի համար, <table> աղյուսակային տվյալների համար։ Բրաուզերը և assistive տեխնոլոգիաները օգտագործում են այդ իմաստը։ Screen reader-ը կարող է թվարկել վերնագրերն ու landmark-ները, որպեսզի օգտատերերը թռչեն էջի մեջ. իսկական կոճակը ստանում է ստեղնաշարի focus և ճիշտ է հայտարարվում առանց լրացուցիչ կոդի. որոնողական համակարգերը հասկանում են կառուցվածքը և հիմնական կոնտենտը։ Այն նաև նվազեցնում է կոդը, որովհետև վարքագիծը ստանում ես անվճար՝ JavaScript-ով և ARIA-ով այն վերակառուցելու փոխարեն։ Ոճավորումն առանձին է, ուստի semantic տարրը կարող է ունենալ ցանկացած տեսք։
Q2Ե՞րբ ես ընտրում Flexbox, և ե՞րբ՝ Grid։
Flexbox-ը մեկ չափման համար է. տարրերի տող կամ սյունակ, որոնց չափը հիմնականում գալիս է իրենց կոնտենտից, օրինակ՝ նավիգացիոն բարեր, toolbar-ներ, կոճակների խմբեր կամ wrap անող tag-երի ցուցակներ։ Grid-ը երկու չափման համար է. երբ ուզում ես նախ սահմանել տողեր և սյունակներ և տարրերը տեղադրել դրանց մեջ, օրինակ՝ էջերի layout-ներ, ապրանքների պատկերասրահներ և dashboard-ներ։ Grid-ը նաև տալիս է responsive սյունակներ առանց media query-ների՝ repeat(auto-fill, minmax(...))-ի միջոցով։ Գործնականում յուրաքանչյուր էջում օգտագործում եմ երկուսն էլ. Grid՝ ընդհանուր կառուցվածքի և քարտերի ցուցակների համար, Flexbox՝ յուրաքանչյուր կոմպոնենտի ներսում հավասարեցման համար։ Երկուսն էլ աջակցում են gap, որը նախընտրում եմ տարրերի միջև margin-ներից։
Q3Բացատրիր event bubbling-ը և event delegation-ը։
Երբ սեղմում ես տարրի վրա, event-ը նախ իջնում է document-ից մինչև թիրախ (capture փուլ), հետո bubble է անում հետ՝ յուրաքանչյուր նախնու միջով։ Հետևաբար ծնողների վրա listener-ները «լսում» են իրենց երեխաների click-երը։ Event delegation-ը օգտագործում է սա. երկար ցուցակի յուրաքանչյուր տողին listener կցելու փոխարեն մեկ listener ես կցում ցուցակին և ստուգում event.target-ը, սովորաբար closest()-ով՝ համապատասխան երեխային գտնելու համար։ Այն ավելի քիչ հիշողություն է օգտագործում և ավտոմատ աշխատում է հետո ավելացված տարրերի համար։ Propagation-ը կարող ես կանգնեցնել stopPropagation()-ով, բայց օգտագործիր այն խնայողաբար, որովհետև այն կոտրում է այլ կոդ, օրինակ՝ անալիտիկան կամ outside-click handler-ները, որոնք հենվում են bubbling-ի վրա։
Q4Ի՞նչ է փոխում box-sizing: border-box-ը։
Լռելյայն (content-box) width-ը և height-ը սահմանում են միայն content-ի տարածքը. padding-ը և border-ը ավելանում են վրան։ 300 պիքսել լայնությամբ box-ը 20 պիքսել padding-ով և 1 պիքսել border-ով իրականում 342 պիքսել լայն է, ինչը layout-ները կոտրում է շփոթեցնող ձևերով։ border-box-ով width-ը ներառում է padding-ը և border-ը, ուստի box-ը ճիշտ 300 պիքսել է, իսկ content-ը փոքրանում է՝ տեղավորվելու համար։ Սա համապատասխանում է դիզայներների մտածելակերպին և տոկոսային layout-ները դարձնում կանխատեսելի։ Գրեթե յուրաքանչյուր ժամանակակից reset կիրառում է border-box բոլոր տարրերի և pseudo-element-ների վրա, և ես նույնն եմ անում յուրաքանչյուր նախագծում։
Q5Ինչո՞ւ fetch-ը սխալ չի նետում 404 կամ 500 պատասխանի դեպքում։
Որովհետև fetch-ի տեսանկյունից հարցումը հաջողվել է. այն հասել է սերվերին և ստացել վավեր HTTP պատասխան։ fetch-ը reject է անում միայն այն ժամանակ, երբ հարցումն ընդհանրապես չի կարողացել ավարտվել, օրինակ՝ երբ ցանցն անջատված է, DNS-ը ձախողվում է, CORS-ն արգելափակում է այն կամ այն abort է արվել։ HTTP սխալի status-ները նորմալ պատասխաններ են, որոնք պետք է ինքդ ստուգես response.ok-ով, որը true է 200-299-ի համար, կամ response.status-ով։ fetch-ը փաթաթում եմ փոքր helper-ի մեջ, որը ստուգում է ok-ը, parse է անում JSON-ը, նետում է տիպավորված սխալ՝ status-ով և body-ով, և աջակցում է AbortSignal և timeout, այնպես որ մնացած կոդը երբեք չի մոռանում այս ստուգումները։
Սենիոր մակարդակի հարցեր
S1Ինչպե՞ս կկառուցես design system, որն աշխատի մի քանի պրոդուկտների և framework-ների միջև։
Այն բաժանում եմ տարբեր կյանքի տևողություն ունեցող շերտերի։ Հիմքը design token-ներն են՝ գույներ, spacing, տառատեսակ, radius-ներ և շարժում, պահված որպես տվյալներ և export արված որպես CSS custom property-ներ, այնպես որ ցանկացած framework կամ նույնիսկ պարզ HTML կարող է դրանք օգտագործել, իսկ theme-երը պարզապես տարբեր արժեքներ են։ Դրանց վրա գալիս են CSS-ի պայմանավորվածությունները՝ cascade layer-ներ, ցածր specificity-ով class-ների անուններ և փաստաթղթավորված breakpoint-ներ։ Կոմպոնենտները գալիս են վերջում և ամենաշատն են կապված framework-ի հետ՝ կառուցված native էլեմենտների վրա accessibility-ի համար։ Փոխզիջումը ճկունության և հետևողականության միջև է. խիստ կոմպոնենտները պրոդուկտները միատեսակ են պահում, բայց հիասթափեցնում են հատուկ կարիքներ ունեցող թիմերին, ուստի ես թույլ եմ տալիս composition և փաստաթղթավորված «փախուստի ելքեր»՝ անվերջ props-ների փոխարեն։ Versioning-ը նույնպես կարևոր է. token-ները փոխվում են հազվադեպ և ապահով, իսկ կոմպոնենտների API-ներին պետք են semver և միգրացիայի նշումներ, ուստի հնարավորության դեպքում դրանք release եմ անում առանձին։
S2Էջն ունի վատ INP։ Ինչպե՞ս ես ախտորոշում և ուղղում այն։
INP-ն չափում է ինտերակցիայից մինչև հաջորդ frame ընկած ուշացումը, ուստի պատճառը միշտ զբաղված main thread-ն է՝ handler-ի աշխատելուց առաջ (input delay), հենց handler-ում (processing) կամ դրանից հետո rendering-ում (presentation delay)։ Սկսում եմ field data-ից՝ real-user monitoring-ի կամ web-vitals գրադարանի attribution-ով, գտնելու համար, թե որ ինտերակցիաներն ու էլեմենտներն են դանդաղ, որովհետև lab թեստերը հազվադեպ են դրանք վերարտադրում։ Հետո վերարտադրում եմ Performance panel-ում CPU throttling-ով։ Ուղղումները կախված են փուլից. հեռացրու կամ հետաձգիր երրորդ կողմի script-ները, որոնք long task-եր են ստեղծում, երկար handler-ները բաժանիր և մասերի միջև yield արա, նախ թարմացրու UI-ը, իսկ ծանր աշխատանքը արա հետո, խուսափիր layout thrashing-ից, փոքրացրու DOM-ի չափը և էկրանից դուրս երկար հատվածների համար օգտագործիր content-visibility։ React հավելվածներում transition-ները և re-render-ի ավելի փոքր scope-երը օգնում են նույն կերպ։
S3Ե՞րբ է progressive enhancement-ը 2026-ին արժե լրացուցիչ ջանքերին։
Այն արժե ամենուր, որտեղ ձախողումը թանկ է, և JavaScript-ը կարող է ձախողվել՝ checkout, գրանցում, որոնման և կոնտակտային ձևեր, ինչպես նաև բովանդակություն, որը պետք է ինդեքսավորվի։ JavaScript-ն ավելի հաճախ է ձախողվում, քան թիմերը կարծում են. դանդաղ ցանցերը timeout են տալիս, extension-ները կոտրում են script-ները, հին սարքերի հիշողությունը վերջանում է, և մեկ runtime սխալը կարող է կանգնեցնել ամբողջ bundle-ը։ Ձևը, որը submit է լինում որպես պարզ HTML և բարելավվում client-side վալիդացիայով ու ակնթարթային արձագանքով, շարունակում է աշխատել այդ բոլոր դեպքերում։ Ժամանակակից framework-ները սա ավելի էժան են դարձնում, քան նախկինում էր, որովհետև server action-ները և native form submission-ը աշխատում են առանց hydration-ի։ Փոխզիջումն այն է, որ բարձր ինտերակտիվ գործիքները, օրինակ՝ էդիտորները, քարտեզները կամ dashboard-ները, քիչ բան են շահում առանց JavaScript-ի fallback-ից, ուստի progressive enhancement-ը կիրառում եմ կրիտիկական ուղիների վրա, ոչ թե որպես համընդհանուր կանոն։
Բառարան
- Render tree
- DOM-ի և CSSOM-ի համադրությունը, որի layout-ը բրաուզերը հաշվարկում և նկարում է. թաքնված էլեմենտները բացառվում են։
- Reflow (layout)
- Box-երի չափերի և դիրքերի վերահաշվարկ փոփոխությունից հետո. թանկ է, երբ ազդում է էջի մեծ մասերի վրա։
- Compositing
- Նկարված շերտերի համադրում GPU-ի վրա. transform-ի և opacity-ի անիմացիան սովորաբար պահանջում է միայն այս քայլը։
- LCP
- Largest Contentful Paint. երբ է ավարտվում ամենամեծ տեսանելի էլեմենտի rendering-ը։ Լավ է 2.5 վայրկյանից պակասը։
- INP
- Interaction to Next Paint. որքան արագ է էջը տեսանելիորեն արձագանքում click-երին, tap-երին և ստեղներին։ Լավ է 200 ms-ից պակասը։
- CLS
- Cumulative Layout Shift. որքան է տեսանելի բովանդակությունն անսպասելիորեն շարժվում էջի օգտագործման ընթացքում։ Լավ է 0.1-ից պակասը։
- Cascade layer
- Style-երի անվանված խումբ. layer-ների հերթականությունը որոշում է առաջնահերթությունը՝ նախքան selector-ների specificity-ն համեմատվի։
- Top layer
- Բրաուզերի կառավարած շերտ ամբողջ բովանդակությունից վեր, որտեղ նկարվում են modal dialog-ները և popover-ները՝ անկախ z-index-ից։
- CSP
- Content Security Policy. response header, որը թվարկում է, թե script-ների, style-երի և frame-երի որ աղբյուրներից կարող է բրաուզերը բեռնել։
- Live region
- Էլեմենտ, որի բովանդակության փոփոխությունները հայտարարում են screen reader-ները. նշվում է aria-live-ով կամ role="status"-ով։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։