---
title: "Вкладки и окна: несколько страниц"
description: "chromedp вкладки: NewContext с вкладкой, Targets для списка открытых страниц, попапы через WaitNewTarget, параллельная работа и cancel против утечек."
course: "Chromedp в Go: автоматизация Chrome без Selenium"
url: "https://golangmaster.ru/courses/chromedp/lessons/16-vkladki/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Вкладки и окна: несколько страниц

*Курс «Chromedp в Go: автоматизация Chrome без Selenium», урок 16 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/chromedp/lessons/16-vkladki/*

В chromedp вкладки — это контексты: один контекст держит одну страницу, и вся работа с несколькими страницами сводится к заведению производных контекстов от браузерного. В этом уроке вы научитесь держать несколько страниц go-программы: одну для навигации, вторую для карточки, третью для внезапного попапа. Разберём, как NewContext порождает вкладки и что у них общее с родителем, что общего у куки и сторожа разных вкладок, а что — собственность каждой страницы, как через chromedp.Targets перечислить открытые цели и подхватить цель, которую сайт открыл сам. Отдельные темы — параллельный обход, где каждая вкладка едет в своей goroutine, и дисциплина закрытия вкладок: без cancel память растёт ровно с темпом обхода.

Человек открывает третий десяток вкладок, не задумываясь, и парсер хочет того же: одна страница для навигации, вторая для карточки товара, третья — внезапный попап, который сайт решил показать. Несколько страниц Go-программа держит тем же способом, что и человек, — вкладками, но под каждой вкладкой chromedp стоит конкретная единица: контекст. Именно контекст определяет, куда приедет следующий Run: у каждого контекста своя цель — target, то есть отдельная страница со своей навигацией и DOM, — и поэтому «работать с двумя вкладками» в терминах библиотеки звучит как «держать два контекста». Каркас при этом уже знаком: NewContext, Run, действия, отмена. Новыми оказываются только две вещи — откуда порождать контекст и когда его закрывать, и обе решаются связкой с родителем. Порядок изучения такой: сначала увидеть, где кончается браузер и начинается вкладка, потом научиться перечислять страницы и подхватывать новые, и в конце — параллелить и закрывать без утечек. К концу урока схема «браузер — вкладка — контекст» будет читаться в коде без усилий, а закрытие вкладки станет планироваться так же заранее, как открытие.

### chromedp вкладки: NewContext от родителя

chromedp.NewContext(parentCtx) создаёт дочерний контекст, и его судьба зависит от родителя: если в родителе уже есть браузер, дочерний контекст получает тот же браузер, но собственную вкладку — первый Run на нём не заводит процесс, а создаёт новую страницу в существующем. Каждый Execute и каждый Run с этим контекстом едут ровно в эту вкладку: навигация, клики и чтения не перелезают на соседние страницы. Второй Run по baseCtx поедет в свою вкладку, не в дочернюю: контексты не переключаются между страницами сами. Родительский контекст при этом продолжает работать со своей вкладкой, как ни в чём не бывало. Внутренности видны через chromedp.FromContext: у обоих контекстов поле Browser одно и то же, а поля Target разные — это точный ответ на вопрос, что общего и что своё. Пример manyTabs из исходников библиотеки печатает ровно это: Same browser: true, Same tab: false. Прогрев вкладки дешёвый — Run(tabCtx) без действий создаёт страницу и готовит её к работе; полезно вызвать его сразу, чтобы вкладка существовала до начала цикла, а ошибки создания ловились в одном месте. Заодно прогрев снимает вопрос «а когда именно появится вкладка»: после него — гарантированно уже.

### Главная вкладка и дочерняя: две goroutines

```go
baseCtx, cancel := chromedp.NewContext(context.Background())
defer cancel()

// прогрев: создаёт браузер и первую вкладку
if err := chromedp.Run(baseCtx); err != nil {
        log.Fatal(err)
}

// вторая вкладка того же браузера
tabCtx, tabCancel := chromedp.NewContext(baseCtx)
defer tabCancel()
if err := chromedp.Run(tabCtx); err != nil {
        log.Fatal(err)
}

var wg sync.WaitGroup
wg.Add(2)
go func() { // вкладка один: свой поток действий
        defer wg.Done()
        err := chromedp.Run(baseCtx,
                chromedp.Navigate(url1),
                chromedp.WaitVisible(".list"))
        log.Println("tab1:", err)
}()
go func() { // вкладка два: независимая страница
        defer wg.Done()
        err := chromedp.Run(tabCtx,
                chromedp.Navigate(url2),
                chromedp.WaitVisible(".list"))
        log.Println("tab2:", err)
}()
wg.Wait()
```

Оба контекста сидят на одном браузере, но у каждого свой Target — своя страница со своим DOM. Действия одного контекста последовательны: два Run по baseCtx из разных goroutines перетасовали бы команды одной вкладки. Разные вкладки — разные потоки: wg дожидается обеих, defer cancel гасит родителя, а вместе с ним и дочернюю вкладку, если та осталась открытой.

> **Живое место: вкладки-призраки.** Типичная утечка: в цикле по адресам на каждый URL создаётся свежий chromedp.NewContext, работа идёт, цикл едет дальше — и никто не вызывает cancel. Через сотню страниц Chrome ест гигабайты: каждая забытая вкладка держит DOM, кучи JavaScript и историю, а рантайм Go видит только маленький хэндл. Диагностика простая: chromedp.Targets показывает список открытых целей, и призраки видны сразу. Лечение ещё проще: tabCancel после каждого шага — вкладка закрыта, память возвращена, браузер продолжает работать, а сессии профиля при этом остаются на месте.

### Что общее, а что своё: куки, сторож, процесс

Вкладки одного профиля — соседи: куки и localStorage у них общие, вход, выполненный в одной вкладке, мгновенно виден в другой, и сессия из тринадцатого урока про [заголовки и куки](/courses/chromedp/lessons/13-zagolovki-kuki/) работает на все страницы сразу. На уровне браузера делится и кэш: картинка, загруженная одной вкладкой, второй достаётся из памяти, а единый интерфейс браузера обслуживает все страницы сразу — настройки, сертификаты, разрешённые сайты. Своё у каждой вкладки — DOM, JavaScript-куча, история навигации и viewport, поэтому селектор, найденный в одной странице, в другой бессмыслен, а взгляд viewport'а одной вкладки не переезжает на соседку. Полная изоляция делается иначе: опция chromedp.WithNewBrowserContext() при NewContext заводит отдельный BrowserContext — собственную куки-банку, где сторож и хранилище не пересекаются с родительскими вкладками. Это профиль-близнец: тот же сайт выглядит незнакомцем, вход нужен заново, зато и следы не смешиваются. Новый BrowserContext уничтожается при отмене контекста, так что такая изоляция не течёт.

### Targets и попап: подхватить то, что открыл сайт

```go
// инвентаризация: что сейчас открыто в браузере
infos, err := chromedp.Targets(ctx)
if err != nil {
        log.Fatal(err)
}
for _, info := range infos {
        log.Printf("%s: %s", info.Type, info.URL)
}

// попап, который сайт откроет по клику
ch := chromedp.WaitNewTarget(ctx, func(info *target.Info) bool {
        return info.Type == "page" && info.URL != ""
})
if err := chromedp.Run(ctx, chromedp.Click("#open-popup")); err != nil {
        log.Fatal(err)
}
popupCtx, popupCancel := chromedp.NewContext(ctx,
        chromedp.WithTargetID(<-ch))
defer popupCancel()

var loc string
if err := chromedp.Run(popupCtx, chromedp.Location(&loc)); err != nil {
        log.Fatal(err)
}
```

Targets возвращает срез target.Info по всем целям браузера: тип и адрес видны сразу, призраки и попапы находятся именно здесь. WaitNewTarget подписывается на события целей и отдаёт в канал ID появившейся страницы: сайт открыл окно — WithTargetID строит контекст прямо на него, и все дальнейшие действия едут в попап, а не в основную вкладку.

### Навигация по существующим: Targets и WithTargetID

Вкладки появляются не только от ваших NewContext: сайт открывает попап, OAuth-окно, всплывашку с капчей — и работать надо уже с тем, что открыл кто-то другой. Первая помощь — chromedp.Targets: действие возвращает срез целей с типом и адресом, по нему видно, сколько страниц открыто и какая из кого. Вторая — chromedp.WaitNewTarget: подписка, которая отдаёт в канал ID только что созданной цели по вашему предикату, — так попап ловится в момент рождения, а не поиском по списку после. Полученный ID скармливается WithTargetID: контекст строится поверх существующей вкладки, и обычный Navigate гоняет её по кругу — это и есть навигация между существующими страницами. И помните про типы: цели бывают не только страницами — iframe и worker тоже фигурируют в списке, поэтому фильтр по Type == page — привычка, которую стоит завести сразу. Тот же приём годится для восстановления: программа перезапустилась, браузер ещё жив, контекст потерян — Targets находит открытые страницы, и работа продолжается с любого из них без нового запуска.

### Утечка вкладок: cancel, память и уборка

Отмена — вторая половина жизни вкладки. chromedp.NewContext возвращает пару контекст и cancel не для красоты: отмена контекста вкладки гасит именно её — библиотека отсоединяет сессию от браузера и отдаёт команде закрытия цели, а родительский браузер при этом не трогает: он живёт, пока жив отец. Забытые cancel — самый тихий путь к росту памяти: каждая вкладка держит свой DOM и JavaScript-кучу, и мусорщик Go здесь бессилен, потому что вкладки живут в чужом процессе. Отсюда дисциплина: defer cancel для вкладки, чья жизнь равна жизни программы, и явный tabCancel сразу после шага для вкладок-однодневок из цикла. Особый случай — WithNewBrowserContext: отдельный BrowserContext удаляется вместе с контекстом, и несделанный cancel оставляет после себя целую куки-банку. Если память всё же поползла — chromedp.Targets покажет, сколько целей открыто на самом деле, и список призраков станет точным счётом вашей забывчивости.

### Задачи вокруг нескольких страниц

| Задача | Механизм | Нюанс |
| --- | --- | --- |
| новая вкладка | NewContext от контекста с браузером | первый Run создаёт вкладку, не браузер |
| список открытых страниц | chromedp.Targets(ctx) | срез target.Info: тип и адрес каждой цели |
| подхватить попап | WaitNewTarget + WithTargetID | канал отдаёт ID новой цели по предикату |
| изолированные куки | NewContext с WithNewBrowserContext | свой профиль: куки и сторож не общие |
| закрыть вкладку | cancel контекста вкладки | браузер живёт, пока жив родитель |
| параллельный обход | вкладка на goroutine | один контекст — один поток действий |

### Параллельный обход: вкладка на goroutine

Схема из первого примера масштабируется: сколько вкладок — столько независимых потоков работы, и ожидание медленной страницы больше не тормозит быструю. Правило безопасности простое: один контекст обслуживается одной goroutine — действия контекста последовательны по своей природе, и два Run по одному контексту из разных goroutines перемешают команды в пределах одной вкладки. Параллелить нужно контексты, а не действия. На этом держится и пул из девятнадцатого урока про [параллельный пул](/courses/chromedp/lessons/19-parallelnyj-pul/): N браузеров, у каждого M вкладок, каждая единица работы — контекст в goroutine. Но вкладки — не бесплатный параллелизм: все они живут в одном процессе Chrome и делят его процессор и память, поэтому десять goroutines на одной вкладке — гонка, а пятьдесят вкладок в одном браузере — движение к OOM. Ожидания, таймауты и ретраи из предыдущего урока действуют на каждую вкладку независимо, и дедлайн одной страницы не задевает соседнюю. Разумный потолок — единицы вкладок на браузер, а дальше добавляйте браузеры: если задача не влезает в один Chrome, не мучайте вкладки сверх меры — следующий уровень масштаба — отдельные браузеры из аллокатора.

> **Про песочницу: окна там не живут.** Тематическая песочница исполняет учебную программу на чистом Go без браузера: вкладок, попапов и реального Chrome в ней нет, и честно не воспроизвести ни WaitNewTarget, ни утечку вкладок. Всё из этого урока проверяется локально: установленный Chrome из второго урока, любой сайт с кнопкой window.open — и попап ловится по-настоящему. Годится и чистый пример без попапа: две вкладки с навигацией уже показывают разделение контекстов. Для повторения примеров удобен httptest-сервер: страница с onclick='window.open' даёт безопасный полигон для тренировки подхвата цели.

### Правила гигиены вкладок

- контекст — вкладка: NewContext от родителя с браузером, не от context.Background() случайно
- первый Run новой вкладки — прогрев: Run(tabCtx) без действий сразу после создания
- cancel после шага: вкладка закрыта, память возвращена, браузер продолжает работать
- Targets как инвентаризация: вкладки-призраки видны в списке целей
- изоляция — WithNewBrowserContext: куки и сторож не пересекаются с основным профилем
- параллель — вкладка на goroutine: один контекст обслуживает один поток

### Проверка: что общее у вкладок

**Вопрос:** Пример manyTabs из исходников: ctx2 := chromedp.NewContext(ctx1), оба Run выполнены. Что печатает сравнение c1.Browser == c2.Browser и c1.Target == c2.Target?

- true и true — второй контекст это та же вкладка
- ✅ true и false — браузер один, вкладки разные
- false и true — вкладка общая, браузеры разные
- false и false — NewContext завёл второй процесс Chrome

NewContext от контекста, где браузер уже есть, наследует Browser и получает собственный Target. Поэтому сравнение браузеров даёт true, а сравнение вкладок — false: тот же процесс, отдельная страница. Заводить новый процесс Chrome такой вызов не умеет — для этого есть аллокатор и свой стартовый контекст.

### Проверка: cancel дочернего контекста

**Вопрос:** Родительский контекст держит браузер, дочерний — вторую вкладку. Что произойдёт при отмене дочернего контекста?

- ✅ Закроется вкладка, браузер продолжит работать
- Закроется весь браузер вместе с первой вкладкой
- Ничего: вкладки закрывает только chromedp.Cancel
- Вкладка закроется и сразу создастся пустая на её месте

Отмена дочернего контекста закрывает именно его вкладку — библиотека отсоединяет сессию и отдаёт браузеру команду закрытия цели. Браузер живёт, пока жив родительский контекст: он держит весь процесс. chromedp.Cancel делает то же самое, но возвращает ошибку уборки — разница в отчётности, а не в объёме закрытия.

### Что сделает первый Run второй вкладки

```go
baseCtx, cancel := chromedp.NewContext(context.Background())
defer cancel()
chromedp.Run(baseCtx) // браузер заведён

tabCtx, _ := chromedp.NewContext(baseCtx)
err := chromedp.Run(tabCtx)
```

**Что выведет программа?**

- Заведёт второй процесс Chrome — по браузеру на контекст
- ✅ Создаст вторую вкладку в уже работающем браузере
- Вернёт ошибку: браузер занят первой вкладкой
- Присоединится к первой вкладке и переиспользует её

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

### Соберите подхват попапа

Восстановите каркас: подписка на новые цели, контекст на ID попапа и его освобождение.

```go
ch := chromedp.___(ctx, func(info *target.Info) bool {
        return info.Type == "page" && info.URL != ""
})
popupCtx, popupCancel := chromedp.NewContext(ctx,
        chromedp.___(<-ch))
___ popupCancel()
```

Подсказка: Новые цели ловит WaitNewTarget, ID скармливается WithTargetID, а освобождение планирует defer.

Правильные ответы в пропусках: `WaitNewTarget`, `WithTargetID`, `defer`

### Расставьте каркас двух вкладок

Соберите по порядку: браузер с прогревом, вторая вкладка, две goroutines и ожидание.

Правильный порядок строк:

1. `baseCtx, cancel := chromedp.NewContext(context.Background())`
2. `defer cancel()`
3. `if err := chromedp.Run(baseCtx); err != nil { log.Fatal(err) }`
4. `tabCtx, tabCancel := chromedp.NewContext(baseCtx)`
5. `defer tabCancel()`
6. `if err := chromedp.Run(tabCtx); err != nil { log.Fatal(err) }`
7. `go func() { chromedp.Run(baseCtx, chromedp.Navigate(url1)) }()`
8. `go func() { chromedp.Run(tabCtx, chromedp.Navigate(url2)) }()`
9. `wg.Wait()`

### Карточки: вкладки и окна

- **NewContext от контекста с браузером** — Дочерний контекст = новая вкладка того же браузера: первый Run создаёт страницу, а не процесс. Browser наследуется, Target — собственный.
- **chromedp.Targets** — Срез target.Info по всем целям браузера: тип и адрес каждой страницы. Инвентаризация открытых вкладок и поиск призраков.
- **WaitNewTarget + WithTargetID** — Подписка отдаёт ID новой цели по предикату; WithTargetID строит контекст поверх существующей вкладки — так подхватывают попапы.
- **Отмена контекста вкладки** — cancel закрывает вкладку: сессия отсоединяется, цели уходит команда закрытия. Браузер живёт, пока жив родительский контекст.
- **WithNewBrowserContext** — Отдельный BrowserContext при NewContext: собственные куки и хранилище, вкладка не видит соседей. Уничтожается при отмене контекста.
- **Параллельность вкладок** — Один контекст — одна goroutine: действия контекста последовательны. Параллелят вкладки и браузеры, не действия одной страницы.

Что дальше: вкладки — каркас масштаба, но страницы редко живут в вакууме — они ходят в сеть. Семнадцатый урок смотрит на эту сторону: [перехват сетевых запросов](/courses/chromedp/lessons/17-perehvat-zaprosov/) — события сети, тела XHR-ответов и блокировка трекеров. Восемнадцатый продолжает тему экономии — [блокировка картинок и шрифтов](/courses/chromedp/lessons/18-blokirovka-resursov/) — и собирает лёгкий профиль страницы. Практика — локально: песочница сайта работает без браузера, поэтому вкладки и попапы проверяются только на настоящем Chrome; в песочнице нет ни окна, ни цели, которую можно подхватить. Домашняя проверка на честность: откройте циклом десять вкладок без cancel, вызовите chromedp.Targets и посчитайте призраков, затем добавьте tabCancel и убедитесь, что список опустел, а память браузера вернулась к стартовой. Цель всех проверок одна: почувствовать, что вкладка — ресурс, который вы создаёте и возвращаете, а не фон, живущий сам по себе.

## Коротко: что запомнить

- Вкладка — это контекст: NewContext от родителя с браузером создаёт новую страницу, и все действия контекста едут в неё.
- У вкладок общий Browser и свои Target: браузер один, страницы разные — так отвечает пример manyTabs из исходников.
- Куки и localStorage общие для вкладок профиля; изоляция — WithNewBrowserContext, отдельная банка, уничтожаемая при отмене.
- chromedp.Targets перечисляет открытые цели: инвентаризация, поиск призраков, восстановление после перезапуска.
- Попап подхватывается связкой WaitNewTarget и WithTargetID: контекст строится поверх цели, открытой самим сайтом.
- cancel контекста вкладки закрывает её; без cancel вкладки копятся, и память растёт с темпом обхода.
- Первый Run вкладки — прогрев: Run(tabCtx) без действий создаёт страницу заранее.
- Один контекст — одна goroutine: действия контекста последовательны, параллелят вкладки, а не действия одной страницы.
- Пул из девятнадцатого урока — та же схема в большом масштабе: браузеры на аллокаторах и вкладки на goroutines с рабочим потолком по памяти.

## Домашнее задание

Соберите программу из двух вкладок: браузер с прогревом, дочерний NewContext, две goroutines, каждая возит свою вкладку по трём адресам и пишет время шага. Добавьте страницу с кнопкой window.open: кликните её и подхватите попап через WaitNewTarget и WithTargetID, выведите адрес попапа. Затем испортите программу — уберите tabCancel из цикла на десять адресов — и сравните chromedp.Targets до и после: посчитайте вкладки-призраки. Финал: заведите изолированную вкладку через WithNewBrowserContext и убедитесь, что вход, выполненный в основной вкладке, в ней не виден. Лог времени шагов подскажет, даёт ли параллельность реальный выигрыш на ваших страницах.

## Вопросы и ответы по уроку

### Что сломается при двух Run по одной вкладке из разных goroutines?

Действия контекста последовательны: команды двух goroutines перемешаются в пределах одной вкладки — Navigate одной вместо Navigate другой, клик не туда, чужой DOM под чужими ожиданиями. Ошибок в духе «контекст занят» библиотека не вернёт, поэтому гонку не поймать по логам — она проявляется испорченными шагами. Правило: один контекст — один поток. Параллельную работу делает новая вкладка: NewContext от того же родителя, и у goroutine появляется собственная страница.

### Чем вкладка отличается от окна?

Для chromedp — ничем существенным: и вкладка, и отдельное окно являются целью типа page со своим ID, и контекст через WithTargetID прицепляется к любой. Разница в интерфейсе браузера, а не в протоколе: целевые события, поля target.Info и действия одинаковы. Практически это значит, что вся механика урока — Targets, WaitNewTarget, cancel — работает и с настоящими окнами, и с попапами, которые сайт открыл как отдельные окна.

### Как выбрать: несколько вкладок одного браузера или несколько браузеров?

Вкладки дешевле: они уже в памяти процесса, старт мгновенный, но делят процессор и память одного Chrome и общие куки профиля. Отдельный браузер изолирует всё — ресурсы, профиль, крэши — но стоит на запуск и место в памяти. Разумная лестница: вкладки для лёгкого параллелизма в пределах профиля, WithNewBrowserContext для изоляции сессий, отдельные браузеры из аллокатора для тяжёлых и независимых задач — об этом девятнадцатый урок.
