diff --git a/examples/lazy-panels.php b/examples/lazy-panels.php
new file mode 100644
index 000000000..8d0f8398e
--- /dev/null
+++ b/examples/lazy-panels.php
@@ -0,0 +1,143 @@
+⚡ Normal';
+ }
+
+ public function getPanel(): string
+ {
+ return '
Normal Panel
'
+ . '
'
+ . '
This panel was rendered during the request (eager).
'
+ . '
Time: ' . date('H:i:s') . '
'
+ . '
';
+ }
+}
+
+
+/**
+ * Example: A "heavy" panel that simulates expensive computation.
+ * When registered with lazy: true, getPanel() is NOT called during the request.
+ * Instead, it is rendered in the shutdown function and served via AJAX on click.
+ */
+class HeavyPanel implements IBarPanel
+{
+ public function getTab(): string
+ {
+ return '🐢 Heavy';
+ }
+
+ public function getPanel(): string
+ {
+ // Simulate expensive operation (e.g., database profiling, API calls)
+ usleep(500_000); // 500ms delay
+
+ return '
Heavy Panel (lazy loaded)
'
+ . '
'
+ . '
This panel was rendered after the response (lazy).
';
+ return $html;
+ }
+}
+
+
+// Register panels:
+// Normal panel (eager) — rendered during the request
+Debugger::getBar()->addPanel(new NormalPanel, 'example-normal');
+
+// Heavy panel — lazy: true means getPanel() is deferred to shutdown function
+Debugger::getBar()->addPanel(new HeavyPanel, 'example-heavy', lazy: true);
+
+// Database panel — also lazy
+Debugger::getBar()->addPanel(new DatabasePanel, 'example-database', lazy: true);
+
+?>
+
+
+
Tracy: Lazy Panel Loading Demo
+
+
How it works
+
This demo shows the lazy: true parameter for Debugger::getBar()->addPanel().
+
+
+
⚡ Normal — A regular panel. Its getPanel() is called during the request.
+
🐢 Heavy — A lazy panel simulating a 500ms expensive operation. Content loads on click.
+
🗄️ DB — A lazy panel simulating database query profiling. Content loads on click.
+
+
+
Usage
+
// Register a lazy panel — getPanel() is NOT called during the request
+Debugger::getBar()->addPanel(new MyExpensivePanel, 'my-panel', lazy: true);
+
+
+
Lazy panels have their getTab() called normally (so the tab is always visible),
+but getPanel() is deferred to a shutdown function. The content is stored in the session
+and fetched via AJAX when you click or hover over the panel tab.
+
+
This is useful for panels that perform expensive operations like database profiling,
+API call logging, or heavy data analysis — they won't slow down your page response time.
+
+For security reasons, Tracy is visible only on localhost. Look into the source code to see how to enable Tracy.';
+}
diff --git a/src/Tracy/Bar/Bar.php b/src/Tracy/Bar/Bar.php
index c85df6c74..f848940cf 100644
--- a/src/Tracy/Bar/Bar.php
+++ b/src/Tracy/Bar/Bar.php
@@ -17,13 +17,19 @@ class Bar
{
/** @var IBarPanel[] */
private array $panels = [];
+
+ /** @var array panel ID => lazy flag */
+ private array $lazyPanels = [];
private bool $loaderRendered = false;
/**
* Add custom panel.
+ * @param bool $lazy If true, panel content is rendered after the response is sent
+ * and loaded via AJAX when the user opens the tab. Use for panels
+ * whose getPanel() is expensive and not needed on every request.
*/
- public function addPanel(IBarPanel $panel, ?string $id = null): static
+ public function addPanel(IBarPanel $panel, ?string $id = null, bool $lazy = false): static
{
if ($id === null) {
$c = 0;
@@ -33,6 +39,10 @@ public function addPanel(IBarPanel $panel, ?string $id = null): static
}
$this->panels[$id] = $panel;
+ if ($lazy) {
+ $this->lazyPanels[$id] = true;
+ }
+
return $this;
}
@@ -155,9 +165,14 @@ private function renderPanels(string $suffix = ''): array
foreach ($this->panels as $id => $panel) {
$idHtml = preg_replace('#[^a-z0-9]+#i', '-', $id) . $suffix;
+ $lazy = isset($this->lazyPanels[$id]);
try {
$tab = (string) $panel->getTab();
- $panelHtml = $tab ? $panel->getPanel() : null;
+ if ($lazy && $tab) {
+ $panelHtml = null; // deferred: content is rendered later and loaded on demand via AJAX
+ } else {
+ $panelHtml = $tab ? $panel->getPanel() : null;
+ }
} catch (\Throwable $e) {
while (ob_get_level() > $obLevel) { // restore ob-level if broken
@@ -167,10 +182,11 @@ private function renderPanels(string $suffix = ''): array
$idHtml = "error-$idHtml";
$tab = "Error in $id";
$panelHtml = "
Error: $id
" . nl2br(Helpers::escapeHtml($e)) . '
';
+ $lazy = false;
unset($e);
}
- $panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml];
+ $panels[] = (object) ['id' => $idHtml, 'tab' => $tab, 'panel' => $panelHtml, 'lazy' => $lazy];
}
restore_error_handler();
@@ -178,6 +194,61 @@ private function renderPanels(string $suffix = ''): array
}
+ /**
+ * Renders the content of lazy panels and stores it in the session so it can be
+ * fetched on demand via AJAX when the user opens the panel. Runs after render().
+ * @internal
+ */
+ public function renderLazyPanels(DeferredContent $defer): void
+ {
+ if (!$defer->isAvailable()) {
+ return;
+ }
+
+ set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
+ if (error_reporting() & $severity) {
+ throw new \ErrorException($message, 0, $severity, $file, $line);
+ }
+
+ return true;
+ });
+
+ $obLevel = ob_get_level();
+ $icons = '