Когда верстаешь адаптив, постоянно скачешь между десктопом и мобильной версией: то DevTools в режиме устройства, то ресайз окна, то открыть на телефоне. Десктоп и мобайл при этом никогда не видны одновременно — один прячется, когда смотришь на другой. А при показе работы заказчику демонстрация «узкого окна браузера» по видеосвязи выглядит так себе.
Готовые решения, конечно, есть. Я смотрел на мобильные симуляторы из Chrome Web Store — например «Mobile First»

«Симулятор телефона — мобильный»

и «U-eyes — мобильный симулятор»

Они прекрасно выполняют свою роль: дают набор устройств, вьюпорты, удобный предпросмотр. Но почти все они открывают превью в отдельном окне или в своей собственной области, и ни один не давал главного, что мне было нужно — быстро и удобно посмотреть адаптивное устройство прямо на вкладке, поверх десктопной версии, одновременно с ней. Хотелось видеть именно это: десктоп и телефон рядом, на той же странице, без переключений.
Поэтому я сделал свой инструмент. В статье — как это устроено технически: где были подводные камни и как я их обходил.
Идея
Расширение по клику строит оверлей: рамку телефона/планшета поверх текущей страницы, а внутри рамки — та же самая страница, но в мобильном вьюпорте. Прокрутка, клики и ввод синхронизируются между десктоп-страницей и превью.
Сразу встал главный вопрос: как отрендерить страницу внутри неё самой?
Проблема №1: страница в iframe, которую нельзя зафреймить
Очевидное решение — засунуть тот же URL в iframe и сузить его до ширины телефона. Но большинство сайтов отдают заголовки, которые запрещают встраивание:
X-Frame-Options: SAMEORIGIN / DENY
Content-Security-Policy: frame-ancestors …
iframe с такой страницей просто не загрузится.
Решение — declarativeNetRequest (MV3). Я динамически добавляю правило, которое для запросов превью-фрейма:
срезает X-Frame-Options;
вырезает из CSP только директиву frame-ancestors (не трогая остальной CSP);
подменяет User-Agent на мобильный, чтобы сайт отдал мобильную вёрстку и сработали media-queries, завязанные на UA.
Важные ограничения, чтобы это было безопасно и не «протекало»:
правило живёт только на время открытого превью и снимается при закрытии;
оно ограничено конкретной вкладкой;
никакие запросы не блокируются, не логируются и никуда не отправляются — меняются только заголовки ответа для рендера фрейма.
Проблема №2: оверлей не должен ломать стили сайта
Оверлей (рамка, панель управления, кнопки) рендерится на чужой странице, где какой угодно CSS. Чтобы стили сайта не поехали от моих и наоборот, весь UI живёт в Shadow DOM. Это даёт изоляцию стилей почти бесплатно: мои классы не конфликтуют с классами сайта.
Проблема №3: синхронизация прокрутки, кликов и ввода
Хотелось, чтобы можно было, например, заполнять форму на десктопе и видеть, как она заполняется в телефоне. Поскольку фрейм — это та же страница, я слушаю события на одной стороне и воспроизвожу на другой: scroll, клики по соответствующим элементам, ввод в поля. Тут много нюансов с маппингом элементов и защитой от бесконечных циклов событий (когда синхронизированное действие снова триггерит событие).
Скриншоты
Функция скриншота снимает видимое превью, локально обрезает по рамке устройства и отдаёт картинку как загрузку файла. Всё происходит на устройстве, ничего никуда не грузится.
Запись экрана
Здесь стоит быть честным про текущее состояние.
В сейчас опубликованной версии запись работает «стандартным» путём: через штатный диалог захвата экрана браузера (то самое разрешение от Google, где вы сами выбираете, что записывать). Видео кодируется локально и сохраняется на устройство файлом. Доступны два формата: WebM и MP4 (MP4 пока в тестовом режиме).
Параллельно я допиливаю более «бесшовный» вариант записи — без промпта на расшаривание и без сужения вьюпорта. Архитектурно он выглядит так:
content-скрипт шлёт в background команды recStart / recStop;
захват идёт через chrome.tabCapture + getUserMedia в offscreen-документе (в MV3 у service worker нет доступа к DOM/медиа, поэтому нужен offscreen);
offscreen возвращает мастер-видео, а при скачивании content лениво инжектит муксеры (mp4-muxer / webm-muxer) и через WebCodecs перекодирует мастер в выбранный формат (MP4 H.264 / WebM VP9) и качество (1080/720/480).
Этот путь с ручным энкодером на WebCodecs — отдельная история с настройкой энкодера, таймстампами кадров и сборкой контейнера. Пока он в разработке; в релизе остаётся стабильный вариант со штатным захватом.
Приватность как ограничение архитектуры
Я сознательно делал так, чтобы расширению нечего было «сливать»: нет бэкенда, нет аналитики внутри расширения, нет удалённого кода. Настройки (выбранное устройство, ориентация, масштаб, тема) лежат в chrome.storage.local и не покидают устройство. Список вкладок с открытым превью — в chrome.storage.session, чтобы восстановиться после перезагрузки; это только идентификаторы вкладок, без содержимого и URL.
Разрешения в манифесте — ровно под задачу: activeTab, scripting, declarativeNetRequest, storage, notifications и host-доступ для рендера превью на любом сайте.
Итог
Получилось расширение Mobile View — десктоп и мобильная вёрстка одновременно, прямо на вкладке: синхронизация, скриншоты и запись экрана, всё локально. Оно бесплатное и лежит в Chrome Web Store.

Буду рад фидбэку именно по технической части: как бы вы решали обход X-Frame-Options, синхронизацию событий или перекодирование на WebCodecs иначе? И если найдёте баги на своей ОС/версии браузера — расскажите, очень помогает.
Ссылка на расширение: https://chromewebstore.google.com/detail/mobile-view-mobile-simu/hocbjiaeeijekejepphjihbpogikmofh



















