Stop Counting Pageviews — Start Counting Eyeballs

Stop Counting Pageviews — Start Counting Eyeballs

May 6, 2026 · 8 min read

A user loads your page, scrolls to 20% of the article, gets distracted by a text message, and leaves. Your analytics records a pageview with a 45-second session. That looks like engagement. It was not. The user never saw your CTA, never read your data analysis, and never reached the section that answers the question they came for.

Pageviews measure page loads. The Intersection Observer API measures what users actually see. After deploying scroll-aware engagement tracking across our network, we discovered that our most "popular" posts by pageview count were not our most read posts — and the discrepancy changed our content strategy.

What Intersection Observer Measures

The Intersection Observer API is a browser-native JavaScript API that fires a callback when a specified element enters or exits the viewport. In plain terms: it tells you when something appears on the user's screen.

By placing invisible observer targets at key points in your content — 25%, 50%, 75%, and 100% of the page — you can track exactly how far each user scrolls. Instead of knowing that 10,000 people loaded the page, you know that 8,200 saw the introduction, 5,100 reached the middle, 3,400 made it to the call-to-action, and 1,800 read to the end.

This is a fundamentally different picture of content performance. And it costs nothing to implement.

The Implementation

Step 1: Place observer targets.

Add invisible <div> elements at key positions in your content template:

<div class="scroll-marker" data-depth="25" aria-hidden="true"></div>
<!-- Content continues -->
<div class="scroll-marker" data-depth="50" aria-hidden="true"></div>
<!-- Content continues -->
<div class="scroll-marker" data-depth="75" aria-hidden="true"></div>
<!-- Content continues -->
<div class="scroll-marker" data-depth="100" aria-hidden="true"></div>

In Eleventy, I place these markers in the post template at calculated positions: after the first quarter of content, at the midpoint, at three-quarters, and at the end.

Step 2: Create the observer.

const markers = document.querySelectorAll('.scroll-marker');
const observed = new Set();

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const depth = entry.target.dataset.depth;
      if (!observed.has(depth)) {
        observed.add(depth);
        // Send event to analytics
        if (typeof gtag === 'function') {
          gtag('event', 'scroll_depth', {
            depth_percentage: depth,
            page_path: window.location.pathname
          });
        }
        observer.unobserve(entry.target);
      }
    }
  });
}, { threshold: 0.5 });

markers.forEach(marker => observer.observe(marker));

This script observes each marker, fires a GA4 event when it enters the viewport, and then stops observing it (so each depth is only counted once per session).

Step 3: Add time-in-view tracking.

Scroll depth alone does not capture attention. A user who scrolls quickly to the bottom did not read the content. Add a time component:

const sectionObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target._viewStart = Date.now();
    } else if (entry.target._viewStart) {
      const viewTime = Date.now() - entry.target._viewStart;
      if (viewTime > 2000) { // Only count if viewed 2+ seconds
        gtag('event', 'section_viewed', {
          section_id: entry.target.id,
          view_duration_ms: viewTime,
          page_path: window.location.pathname
        });
      }
      delete entry.target._viewStart;
    }
  });
}, { threshold: 0.5 });

document.querySelectorAll('h2').forEach(h2 => sectionObserver.observe(h2));

This observes every H2 heading and measures how long each section is in the viewport. A section viewed for 2+ seconds counts as "read." A section scrolled past quickly does not.

What We Discovered

The data from Intersection Observer tracking across our 52-site network contradicted several assumptions:

Our longest posts were not our most-read posts. A 3,000-word post with 10,000 pageviews had only 1,200 readers reach the 75% mark. A 1,200-word post with 4,000 pageviews had 2,800 reach 75%. By pageview count, the long post was a winner. By actual readership, the short post was 2.3x more effective.

CTAs placed at 70% scroll depth performed 4x better than CTAs at 90%. We had been placing calls-to-action near the bottom of posts. The data showed that only 18% of readers reached that position. Moving CTAs to the 65-70% mark — where 45% of readers still had attention — more than tripled CTA visibility.

Specific sections had dramatically different engagement. On one post, the section about implementation steps had an average view time of 47 seconds. The section about background theory had an average view time of 8 seconds. Readers wanted the how-to, not the why. This informed our content structure going forward.

Mobile readers scroll less than desktop readers. On mobile, the average scroll depth was 58%. On desktop, it was 72%. Mobile content needs to front-load value even more aggressively than desktop content.

Building a Scroll Engagement Dashboard

Raw GA4 scroll data is hard to interpret. We built a simple dashboard in Looker Studio (formerly Google Data Studio, also free) that visualizes scroll engagement:

Content Completion Rate: For each post, the percentage of pageview users who reached the 100% marker. This is the truest measure of content quality — did people read it through?

Engagement Drop-Off Chart: A funnel visualization showing the percentage of users at each scroll marker (25%, 50%, 75%, 100%). Steep drop-offs between markers indicate sections that lose reader attention.

Section Engagement Heatmap: Average time-in-view for each H2 section across all posts. This reveals which types of sections hold attention and which lose it.

CTA Visibility Rate: The percentage of readers who scroll past each CTA position. This directly informs CTA placement optimization.

The Performance Impact

Intersection Observer is extremely lightweight. Unlike scroll event listeners — which fire on every pixel of scroll movement and can cause jank — Intersection Observer only fires when elements cross visibility thresholds. The browser handles the intersection calculations natively, off the main thread.

In our testing:

  • Total Blocking Time impact: Undetectable (under 1ms)
  • Layout Shift impact: Zero (observer targets are invisible, zero-height elements)
  • Bundle size: The complete script is under 1KB minified
  • Battery/CPU impact: Negligible — the observer is idle when nothing is crossing thresholds

This means you can deploy engagement tracking without any performance penalty. The script is lighter than a single analytics event script.

Why This Matters for Your Next Audit

If your site audit only looks at pageview counts, bounce rates, and session durations, you are evaluating content with incomplete data. Pageviews do not mean readership. Low bounce rate does not mean engagement. Long session duration does not mean attention.

Intersection Observer gives you the data layer that connects "someone loaded this page" to "someone read this content." That connection is the difference between content that generates traffic and content that generates understanding, trust, and conversion.

Deploy it before your next content audit. The data will change what you think your best content is.


The complete Intersection Observer analytics framework — including the dashboard templates, the CTA optimization methodology, and the content scoring system — is in The $20 Dollar Agency by J.A. Watte. Chapter 18 covers analytics beyond pageviews. For deploying engagement analytics across a site network, see The $100 Network.