Technology

Overcoming Magento’s full-page cache through hole punching

Posted by Pixafy Team

Introduction

Recently I was tasked to develop a solution for the following problem: Overlay a registration modal only if the user is on the homepage or for every 5 clicks around the website.

Sounds simple, and in theory it was; however, Magento always likes to throw curve balls when moving a site from local development to a live staging server and the issue this time was full page caching. Before diving into what full page caching is, let me explain briefly how the module worked.

An observer was setup to run a function after the event controller_action_predispatch was fired. This function would either add the current page to an array as a key or if the page key was found, increment the number of times the page was visited. This information was then encoded into JSON and stored in a session to access later.

In order to determine when the pop-up modal should appear, a simple function would tally up the total page counts and total homepage counts.

Now that we’ve established the basics of the module, let’s go over how Magento’s full page caching works.

Full Page Caching

I should put in here that I’m not a complete expert on this subject. What I know is simply a collection of my studies and personal practice using Magento.

Full page caching is a way for Magento to load content for a user without having to fully initialize the application. If you can picture loading a webpage, taking a screenshot, and then storing that screenshot to bring up again later, you could loosely describe the concept of full page caching. This makes the entire website blazing fast, but makes it less dynamic. Magento has built-in ways to achieve a more dynamic page through a process called “hole-punching.”

Hole-Punching

To completely understand hole-punching, you should have a solid understanding of the Magento design structure. The typical page is composed of block and template files. Blocks and templates coordinate directly with each other to display content to a user in usually a very modular and dynamic fashion. To keep this dynamic relationship active with FPC on we are given the ability to surround our block and template files with something called a placeholder, or container. Think of a placeholder as a way of telling Magento, “Hey, I have this content here that is dynamic, so I may need your help retrieving it.”

To do retrieve content, containers have two major functions that are important to us: applyInApp($content) and applyWithoutApp($content) – These functions are responsible for finding your placeholders and plugging in the content that you specified based on an identifier and cache lifetime. It is always best to look directly at the source in Enterprise_PageCache_Model_Container_Abstract to understand how these functions work in whole.

Now that we know how a typical Magento page is composed, let’s begin dive into the mysteries behind full page caching. There’s a great article you all should read if you haven’t already that thoroughly explains the ins and outs of full page caching and why you should be using it if you’re not already. In sum, here are the basics. Magento FPC knows of four states:

  1. Page is in cache with no dynamic blocks
  2. Page is in cache, dynamic blocks are cached
  3. Page is in cache, dynamic blocks are not cached
  4. Page is not in cache

Depending on the page, content, and cache lifetimes, Magento will handle each request slightly different.

State 1: Page is in cache with no dynamic blocks: Magento gets a request to load a static webpage. Magento finds that it has this particular page in its FPC bank and presents it to the user. Viola! Magento did a minimal amount of work to achieve a page load and your server is very happy.

State 2: Page is in cache, dynamic blocks are cached: Very similar to State 1, Magento gets a request to load a webpage and finds the page is cached. Magento then comes across a container for a dynamic block. From here it determines if the block is cached or not and if so, will proceed to fill the container with the cached block using applyWithoutApp($content). Presto! We have another full page load with very minimal overhead and your server thanks you.

State 3: Page is in cache, dynamic blocks are not cached: Here is where things start to get tricky. If Magento was not able to find the cached block like in State 2 using applyWithoutApp($content) then it needs to generate the block using the appropriate class and template files. At this point, Magento needs to be initialized to serve the content. A request is sent to pagecache/request/process and default routing and processing takes place. The RequestController handles our request and eventually hits the method applyWithApp($content), where our block is instantiated and rendered using $this->_renderBlock(). Now Magento can replace the container with our static block and if possible, cache it to use later.

State 4: Page is not in cache: At this point Magento did not find the requested page in the cache, so the page must be processed as usual. Dynamic blocks will be surrounded with their appropriate placeholders using the cache.xml files specified. These placeholders contain the data that full-page caching used in States 2 and 3, such as what block and template files to use as well as a cache identifier (usually cache_id).

Lost? You’re Not Alone

If you’re still lost on how Magento full-page caching works, it is always best to look at a working example.  I wrote a small module for this article available for download that simply outputs some dynamic data at the top of the page. If you enable FPC, the dynamic content should cache appropriately, displaying a different set of “lucky numbers” every so often. It’s not magic, but almost feels like it.

Conclusion

This article scratches the surface of the full potential of Magento’s full-page caching. To really understand the power of FPC you should dig into the core source code, particularly the Enterprise/PageCache module, and practice. Happy hacking!

Have you fought full-page caching and won? Share your experiences and tips in the comments!