Ephes Blog

Miscellaneous things. Mostly Weeknotes and links I stumbled upon.

Date -

Podlove Web Player Performance Improvements

, Jochen

I recently attended the SUBSCRIBE 11 podcasting conference, where I had the opportunity to chat with the author of Pods-Blitz about the Podlove Web Player. After we both listened to a talk about it, he asked me how I liked the Web Player. I mentioned that I really enjoyed it but suspected it was slowing down my website. He immediately agreed, noting that he had observed the same behavior.

Here's a screenshot illustrating the issue:

python-podcast-performance_original_scores

The poor performance seems to stem from the player loading a substantial amount of JavaScript and CSS assets.

podlove-player_slow_assets

Today, I decided to ask on Discord what might be causing this problem. Below is my original JavaScript code used to initialize the five audio players on the overview page of my podcast:

<script defer src="/static/cast/js/web-player/embed.5.28903a742256.js"></script>
<script>
  function initializePodlovePlayers() {
    document.querySelectorAll('section.block-audio div[id^="audio_"]').forEach(div => {
      const audioId = div.id;
      const url = div.getAttribute('data-url');
      podlovePlayer(`#${audioId}`, url, "/api/audios/player_config/");
    });
  }

  document.body.addEventListener('htmx:afterSwap', (event) => {
    if (event.detail.target.id === 'paging-area') {
      initializePodlovePlayers();
    }
  });

  // Call the function on initial load
  document.addEventListener('DOMContentLoaded', (event) => {
    initializePodlovePlayers();
  });
</script>

Alexander Heimbuch suggested that I use the load event instead of DOMContentLoaded to avoid blocking the main thread. He also advised using the IntersectionObserver API to defer the initialization until the player becomes visible in the viewport. I'm really grateful for Alex's help—it made a significant difference in optimizing my website. Based on his suggestions, I updated my code:

<script defer src="{% static 'cast/js/web-player/embed.5.js' %}"></script>
<script>
  function initializePodlovePlayerWhenVisible(div) {
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const audioId = div.id;
          const url = div.getAttribute('data-url');
          podlovePlayer(`#${audioId}`, url, "/api/audios/player_config/");
          observer.unobserve(div); // Stop observing since we don't need to initialize again
        }
      });
    });
    observer.observe(div);
  }

  function initializePodlovePlayers() {
    document.querySelectorAll('.podlove-player-container').forEach(div => {
      initializePodlovePlayerWhenVisible(div);
    });
  }

  document.body.addEventListener('htmx:afterSettle', (event) => {
    if (event.detail.target.id === 'paging-area') {
      setTimeout(() => {
        initializePodlovePlayers();
      }, 1500); // Adjust the delay as needed -> wait to finish scrolling up
    }
   });

  // Attach to the load event to ensure the page has fully loaded
  window.addEventListener('load', function() {
    initializePodlovePlayers();
  });
</script>

These changes made a significant difference—the performance score for mobile increased from 60 to 83. Realizing that it wouldn't be easy to reduce the connections to cdn.podlove.org—since each player is isolated in its own iframe and the URL is hardcoded in the pre-built player bundle—I added an additional optimization by including a preconnect link in the header of my website:

<link rel="preconnect" href="https://cdn.podlove.org" />

This also helped a lot to improve performance. Here are the final results:

python-podcast_optimized_scores

After grappling with htmx and pagination issues, I decided to encapsulate all the JavaScript into a web component. Now, the player script is dynamically imported when the web component becomes visible.

{% load static %}
{% if page.pk %}
  <podlove-player
    id="audio_{{ value.pk }}"
    data-variant="xl"
    data-url="{% url 'cast:api:audio_podlove_detail' pk=value.pk post_id=page.pk %}"
    data-embed="{% static 'cast/js/web-player/embed.5.js' %}"
    data-config="{% url 'cast:api:player_config' %}"
  >
  </podlove-player>
{% endif %}
// podlove-player.ts
class PodlovePlayerElement extends HTMLElement {
  constructor() {
    super();
    this.observer = null;
    this.shadow = this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.renderPlaceholder();

    if (document.readyState === 'complete') {
      // The page is already fully loaded
      this.observeElement();
    } else {
      // Wait for the 'load' event before initializing
      window.addEventListener('load', () => {
        this.observeElement();
      }, { once: true });
    }
  }

  disconnectedCallback() {
    if (this.observer) {
      this.observer.disconnect();
    }
  }

  renderPlaceholder() {
    // Reserve space to prevent layout shifts
    const container = document.createElement('div');
    container.classList.add('podlove-player-container');

    // Apply styles
    const style = document.createElement('style');
    style.textContent = `
      .podlove-player-container {
        width: 100%;
        max-width: 936px;
        height: 300px;
        margin: 0 auto;
      }
      @media (max-width: 768px) {
        .podlove-player-container {
          max-width: 366px;
          height: 500px;
        }
      }
      .podlove-player-container iframe {
        width: 100%;
        height: 100%;
        border: none;
      }
    `;

    this.shadow.appendChild(style);
    this.shadow.appendChild(container);
  }

  observeElement() {
    this.observer = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.initializePlayer();
          observer.unobserve(this);
        }
      });
    });
    this.observer.observe(this);
  }

  initializePlayer() {
    const container = this.shadow.querySelector('.podlove-player-container');
    const audioId = this.getAttribute('id') || `podlove-player-${Date.now()}`;
    const url = this.getAttribute('data-url');
    const configUrl = this.getAttribute('data-config') || '/api/audios/player_config/';
    const podloveTemplate = this.getAttribute('data-template');
    let embedUrl = this.getAttribute('data-embed') || 'https://cdn.podlove.org/web-player/5.x/embed.js';

    // If host ist localhost use local embed url
    const { protocol, hostname, port } = window.location;


    console.log("data template: ", podloveTemplate);
    const playerDiv = document.createElement('div');
    playerDiv.id = audioId;

    // set the template attribute if it is set (needed for pp theme)
    if (podloveTemplate !== null) {
        playerDiv.setAttribute('data-template', podloveTemplate);
    }
    container.appendChild(playerDiv);

    if (typeof podlovePlayer === 'function') {
      podlovePlayer(playerDiv, url, configUrl);
    } else {
      // If in dev mode on localhost and embedUrl starts with a slash, use the local embedUrl
      // otherwise the vite url 5173 will be used -> which will not work
      if (hostname === 'localhost' && embedUrl.startsWith("/")) {
        embedUrl = `http://localhost:${port}${embedUrl}`;
      }
      // Dynamically load the Podlove player script
      import(embedUrl).then(() => {
        podlovePlayer(playerDiv, url, configUrl);
      });
    }
  }
}

console.log("Registering podlove-player!");
customElements.define('podlove-player', PodlovePlayerElement);

Weeknotes 2024-10-28

, Jochen
From Facebook .....
I just learned the professional way to say "I told you so":
"This was identified early on as a likely outcome." --Christos Argyropoulos MD, PhD

Last week, I wrote a lot of Playwright end-to-end tests, and I really appreciate how easy it is these days to copy a snippet of HTML from a website and simply ask an LLM, 'How do I click the submit button?' or 'How can I select option "x" with Playwright?' It quickly generates a working line of code that I can drop straight into my test.

Articles

Videos

Fediverse

Software

Out of Context Images


Weeknotes 2024-10-21

, Jochen
ACAB includes Paw Patrol. --Prof Kemi FG

I attended the local Django Meetup in Cologne and listened to an interesting talk about Hyperview, which is based on React Native. It's probably more of an early adopter thing at the moment, but the idea is really intriguing.

I published a podcast episode about arrays and sequences , which had probably the longest gap between recording and publishing we've ever had—but I just wasn't able to do it earlier. We revisit the first chapters of Fluent Python because, why not? This book is really good.

On the infrastructure side, I updated all my databases to PostgreSQL 17 and found my old post about upgrading PostgreSQL still helpful. I'm also using uv now in production deployments, and it works really great. This uv thing looks like it will simplify a lot of workflows for me—yay for that!

Then I attended SUBSCRIBE 11, a podcasting-related conference. The conference was great, the weather was superb, and I talked to a lot of people about podcast publishing software. I even accidentally held a workshop about pods-blitz, a great new podcast publishing software written in Rust by Ulrich Dürholz. Here's a cool list of all the podcasts whose hosts attended.

Articles

Videos

Fediverse


Weeknotes 2024-10-14

, Jochen
"To whom it may concern"
- vague
- weak
- ignorable
"To whom it will concern"
- ominous
- strong
- alarming
--Aelfred the Great

Released a new version of django-cast with support for Python 3.13, which was just released last week. Made some progress on my frontend-heavy side project using htmx (still not published). Otherwise, it was a fairly typical work week.

Articles

Videos

Fediverse


Weeknotes 2024-10-07

, Jochen
hey sorry I missed your text, I am processing a non-stop 24/7 onslaught of information with a brain designed to eat berries in a cave --Janel Comeau

I spent some time outdoors and noticed mushrooms growing everywhere—so cool! I have also been working on an upcoming project that should be easily extensible with plugins. Lets see how that works out.

Articles

Weeknotes

Videos

Software