How does Full Page Cache(FPC) work?

Full Page Cache is one of the Magento core features that significantly boosts the speed of your store. Furthermore, this feature is available out-of-the-box. When the page cache is hit, Magento skips FrontController and just returns the response. We are going to describe how this works on code level.

Recommended Extension

An Around Plugin

The core part is an around plugin for \Magento\Framework\App\FrontController::dispatch in Magento_PageCache module.

\Magento\PageCache\Model\App\FrontController\BuiltinPlugin::aroundDispatch

/**
 * Add PageCache functionality to Dispatch method
 *
 * @param \Magento\Framework\App\FrontControllerInterface $subject
 * @param callable $proceed
 * @param \Magento\Framework\App\RequestInterface $request
 * @return \Magento\Framework\Controller\ResultInterface|\Magento\Framework\App\Response\Http
 * @SuppressWarnings(PHPMD.UnusedFormalParameter)
 */
public function aroundDispatch(
    \Magento\Framework\App\FrontControllerInterface $subject,
    \Closure $proceed,
    \Magento\Framework\App\RequestInterface $request
) {
    $this->version->process();
    if (!$this->config->isEnabled() || $this->config->getType() !== \Magento\PageCache\Model\Config::BUILT_IN) {
        return $proceed($request);
    }
    $result = $this->kernel->load();
    if ($result === false) {
        $result = $proceed($request);
        if ($result instanceof ResponseHttp && !$result instanceof NotCacheableInterface) {
            $this->addDebugHeaders($result);
            $this->kernel->process($result);
        }
    } else {
        $this->addDebugHeader($result, 'X-Magento-Cache-Debug', 'HIT', true);
    }
    return $result;
}

The Logic

$result = $this->kernel->load(); loads the full page cache. If $result is not false, that is, the cache was found, this plugin will not call $proceed, which means the whole FrontController is skipped and the cache is used directly. The web page loads faster due to the skipping of all processing in FrontController.

$this->addDebugHeader($result, 'X-Magento-Cache-Debug', 'HIT', true); adds debug header in developer mode. So you can confirm if the cache is used.

$result = $this->kernel->process(); saves the full page cache if it is not available. So the next time, it will be HIT.

Full Page Cache Saving Conditions

The conditions may not be obvious, the full page cache can only be saved if the following conditions are met.

  • HTML response
  • HEAD or GET request
  • The response HTTP status code is 200 or 404
  • Public content(i.e, not customer/checkout page)
  • (No block has cacheable="false" in layout XMLs)

X-Magento-Vary Cookie

If you go deeper by looking at the following methods.

\Magento\PageCache\Model\App\PageCache\Kernel::process

/**
 * Modify and cache application response
 *
 * @param \Magento\Framework\App\Response\Http $response
 * @return void
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
 */
public function process(\Magento\Framework\App\Response\Http $response)
{
    $cacheControlHeader = $response->getHeader('Cache-Control');
    if ($cacheControlHeader
        && preg_match('/public.*s-maxage=(\d+)/', $cacheControlHeader->getFieldValue(), $matches)
    ) {
        $maxAge = $matches[1];
        $response->setNoCacheHeaders();
        if (($response->getHttpResponseCode() == 200 || $response->getHttpResponseCode() == 404)
            && !$response instanceof NotCacheableInterface
            && ($this->request->isGet() || $this->request->isHead())
        ) {
            $tagsHeader = $response->getHeader('X-Magento-Tags');
            $tags = $tagsHeader ? explode(',', $tagsHeader->getFieldValue() ?? '') : [];

            $response->clearHeader('Set-Cookie');
            if ($this->state->getMode() != AppState::MODE_DEVELOPER) {
                $response->clearHeader('X-Magento-Tags');
            }
            $this->cookieDisabler->setCookiesDisabled(true);

            $this->fullPageCache->save(
                $this->serializer->serialize($this->getPreparedData($response)),
                $this->identifierForSave->getValue(),
                $tags,
                $maxAge
            );
        }
    }
}

\Magento\PageCache\Model\App\PageCache\Identifier::getValue

/**
 * Return unique page identifier
 *
 * @return string
 */
public function getValue()
{
    $pattern = $this->getMarketingParameterPatterns();
    $replace = array_fill(0, count($pattern), '');
    $url = preg_replace($pattern, $replace, (string)$this->request->getUriString());
    list($baseUrl, $query) = $this->reconstructUrl($url);
    $data = [
        $this->request->isSecure(),
        $baseUrl,
        $query,
        $this->request->get(\Magento\Framework\App\Response\Http::COOKIE_VARY_STRING)
            ?: $this->context->getVaryString()
    ];
    return sha1($this->serializer->serialize($data));
}

See this part.

$this->request->get(\Magento\Framework\App\Response\Http::COOKIE_VARY_STRING)
    ?: $this->context->getVaryString()

You will notice that the full page cache identifier even depends on the X-Magento-Vary cookie.

Closing Thoughts

To maximize your store's performance, it's recommended to use a CDN. There are free CDNs even suitable for medium size store. If you don't want to use CDN, at least, use Varnish.

If you found this blog post helpful, please share it!