GPT Lazy Load Generator for Google Publisher Tag

Configure Google Publisher Tag lazy loading, size mapping and viewable-only refresh in a form, and get an async snippet that reserves space for every slot. Margin settings that quietly lower your viewability rate are flagged before you ship them.

100% free · No sign-up · No watermarks

Network & API

Lazy loading

Margins are a percentage of the viewport height.

Ad slots

One per ad position on the page.

Slot 1

Size mapping: from viewport width → sizes (blank = no ad)

px →
px →
Slot 2

Request options

Where ads load

Distance below the screen at which GPT fetches, then renders.

Desktopscreenrender 1×fetch 2×Mobilescreenrender 2×fetch 4×

Checks

0 warnings

With single request on, Google notes that lazy fetching only works when all slots are outside the fetch margin. Slots already near the screen on load are requested together in one call.

Refresh is for Ad Manager. AdSense does not allow refreshing ad units, and some Ad Manager line items and buyers exclude refreshed inventory — declare the refresh key-value so they can.

Paste in <head>

<!-- Google Publisher Tag. async, so it never blocks rendering. -->
<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous"></script>
<script>
  window.googletag = window.googletag || { cmd: [] };

  googletag.cmd.push(() => {
    const slot1 = googletag
      .defineSlot("/1234567/homepage/top_leaderboard", [[728, 90], [970, 90], [320, 50]], "div-gpt-top")
      .addService(googletag.pubads());
    slot1.defineSizeMapping(
      googletag
        .sizeMapping()
        .addSize([1024, 0], [[728, 90], [970, 90]])
        .addSize([0, 0], [320, 50])
        .build()
    );

    const slot2 = googletag
      .defineSlot("/1234567/homepage/in_content", [[300, 250], [336, 280]], "div-gpt-content")
      .addService(googletag.pubads());

    googletag.setConfig({
      singleRequest: true,
      collapseDiv: "ON_NO_FILL",
      lazyLoad: {
        // Fetch slots within 2 viewports of the screen.
        fetchMarginPercent: 200,
        // Render slots within 1 viewport.
        renderMarginPercent: 100,
        // Multiply both margins on mobile, where people scroll faster.
        mobileScaling: 2,
      },
    });

    // Viewable refresh. Declare the "refresh" key-value in Ad Manager so
    // refreshed impressions can be reported on and excluded by buyers.
    const REFRESH_MS = 30000;
    const refreshable = new Set(["div-gpt-content"]);
    const slotsById = new Map();
    const refreshCount = new Map();
    const inView = new Set();
    const due = new Set();

    const observer = new IntersectionObserver(
      (entries) => {
        for (const entry of entries) {
          const id = entry.target.id;
          if (entry.intersectionRatio >= 0.5) {
            inView.add(id);
            if (due.has(id)) refreshIfSeen(id);
          } else {
            inView.delete(id);
          }
        }
      },
      { threshold: [0, 0.5] }
    );

    function refreshIfSeen(id) {
      if (document.visibilityState !== "visible" || !inView.has(id)) {
        due.add(id);
        return;
      }
      due.delete(id);
      const slot = slotsById.get(id);
      const count = (refreshCount.get(id) || 0) + 1;
      refreshCount.set(id, count);
      slot.setConfig({ targeting: { refresh: String(count) } });
      googletag.pubads().refresh([slot]);
    }

    document.addEventListener("visibilitychange", () => {
      due.forEach(refreshIfSeen);
    });

    googletag.pubads().addEventListener("impressionViewable", (event) => {
      const id = event.slot.getSlotElementId();
      if (!refreshable.has(id)) return;
      slotsById.set(id, event.slot);
      const el = document.getElementById(id);
      if (el) observer.observe(el);
      setTimeout(() => refreshIfSeen(id), REFRESH_MS);
    });

    googletag.enableServices();
  });
</script>

Paste where each ad appears

<!-- /1234567/homepage/top_leaderboard -->
<style>
  #div-gpt-top { min-height: 50px; }
  @media (min-width: 1024px) { #div-gpt-top { min-height: 90px; } }
</style>
<div id="div-gpt-top">
  <script>
    googletag.cmd.push(() => {
      googletag.display("div-gpt-top");
    });
  </script>
</div>

<!-- /1234567/homepage/in_content -->
<div id="div-gpt-content" style="min-height: 250px;">
  <script>
    googletag.cmd.push(() => {
      googletag.display("div-gpt-content");
    });
  </script>
</div>

The test page spaces your slots a screen and a half apart. Open it, keep the Network panel filtered to “gampad”, and scroll: each ad request should fire as its slot nears the screen. Your viewability rate itself lives in Ad Manager’s Active View reports.

Why use the GPT Lazy Load Generator

Private by design

No account needed. What you type stays in your browser unless you ask for an AI suggestion or name a URL to fetch.

Current GPT API

Generates googletag.setConfig() code, with the legacy enableLazyLoad form available for older setups.

No layout shift

Every slot reserves its height, so an ad loading in does not push the page down and add to CLS.

Frequently asked questions

What counts as a viewable impression?

The IAB and MRC standard for display ads is at least 50% of the ad's pixels on screen for at least one continuous second; for large ads of 242,500 pixels or more it is 30%. Video needs two continuous seconds. Viewability rate is viewable impressions divided by measurable ones, so every ad served where nobody looks pulls the rate down.

What do fetch margin and render margin mean?

Both are distances from the visible screen, as a percentage of its height. When a slot comes within the fetch margin GPT requests the ad; within the render margin it paints the creative. A fetch margin of 200 means two screens ahead. mobileScaling multiplies both on phones, where people scroll faster.

Can a bigger fetch margin hurt viewability?

Yes, and it is the most common way lazy loading is misconfigured. A wide margin means ads are requested for slots the reader may never scroll to. Those impressions are served and measured but never seen. Tighter margins mean fewer impressions, but a larger share of them are seen, and that share is what buyers bid on.

Does this work with AdSense?

Not directly. These are Google Publisher Tag settings, used with Google Ad Manager, including Ad Manager accounts that backfill with AdSense. Ordinary AdSense ad units use the adsbygoogle tag and do not expose lazy-load margins. AdSense also does not allow refreshing ad units, so leave refresh off for any AdSense inventory.

Will lazy loading fix unfilled impressions?

Partly. It stops requests for slots nobody reaches, which removes unfilled impressions caused only by requesting too much inventory. It cannot fix demand: if line items, pricing rules or targeting do not match a request, it goes unfilled however late you make it. Collapse on no fill hides the empty box when that happens.

setConfig or enableLazyLoad?

Google now marks pubads().enableLazyLoad(), enableSingleRequest(), collapseEmptyDivs() and disableInitialLoad() as deprecated, replaced by googletag.setConfig() with lazyLoad, singleRequest, collapseDiv and disableInitialLoad. The old calls still work, and most tutorials use them, so both forms are available here. Use setConfig for new work.

Why reserve space for each slot?

An ad that loads into a zero-height div pushes everything below it down, and that jump counts toward Cumulative Layout Shift, a Core Web Vital. A min-height equal to the smallest creative removes most of it. With a size mapping the generator sets a height for each breakpoint, because a mobile banner and a desktop leaderboard need different heights.

Updated · Built by Niraj Kumar