Apostrophe 1.5: legacy documentation

Still using Apostrophe 1.5? Check out Apostrophe 2 for your future projects.

Performance

Maximizing Apostrophe's Performance

Apostrophe is friendly and powerful. It can also be fast. Speed should never be your sole priority in choosing a CMS, but we recognize that a site must be responsive in order to be usable. Let's talk about ways to make sure your site is as fast as it needs to be.

Don't Even THINK About Running Without APC

If your PHP hosting is set up badly, PHP is forced to read and "recompile" nearly single line of Apostrophe and Symfony on every single page access. This is ridiculous and slow. It is also completely avoidable.

Properly configured PHP hosting uses "APC" (the so-called "Alternative PHP Cache," which is actually the mainstream solution these days) to remember everything it learned last time it compiled a PHP file, so that your code does not have to be compiled over and over again. This is essential. But many sites are running without it, experiencing bad performance and blaming their CMS framework, whether it's Apostrophe, Drupal or even Wordpress.

 Consult this article to learn everything we know about configuring your webserver and PHP for maximum PHP performance. The really important part is to enable APC and configure Apache reasonably; you can skip the stuff about fastcgi if you're not feeling hardcore. Good webhosts like  ServerGrove often enable APC out of the box. It is usually not available with "shared hosting." You should always go with VPS hosting.

Note that the servercheck.php page provided with the Apostrophe sandbox can be used to check whether APC is enabled.

 We also have an article on Windows PHP performance for those who host Apostrophe on Windows.

Caching pages with app_a_page_cache_enabled

Once you have APC in place and your Apostrophe site is running as fast as it should, it is still fair to point out that under really heavy traffic loads, the site is doing more work than necessary. After all, everyone is getting the same home page if they are logged out... right? That's true (mostly), and our optional aCacheFilter feature allows you to leverage that fact to make the website much faster and keep your hosting costs down.

aCacheFilter drastically speeds up high-traffic Apostrophe sites by caching all content for GET requests for five minutes for logged-out users. This is great, but it does require that your editors clearly understand that their edits will not be seen by logged out users for up to five minutes. This simple strategy avoids many, many deeply complex issues with cache invalidation. But since it requires user education and is not necessary on low-traffic sites we do not enable it by default in our sandbox project.

aCacheFilter does not yield as big a speedup as a static cache, but also does not have cache invalidation problems once the user logs into the same page they just saw as a logged-out user. It is a caching solution for logged-out users, designed to play nicely with Apostrophe's in-context editing philosophy for logged-in users.

(Note that you will not see a performance benefit if no one else has accessed the page in question in the past five minutes. If your site does not receive high traffic you probably do not need this feature.)

Activate aCacheFilter in apps/frontend/config/filters.yml like so:

rendering: ~
security:  ~

# insert your own filters here
aCache:
  class: aCacheFilter
  
cache:     ~
execution: ~

Now turn it on in app.yml for staging and production environments. You can leave it shut off for the dev environment. (The requirement to enable it in app.yml was added to the 1.5 stable branch on 2011-06-22.)

dev:
  a:
    page_cache_enabled: false

staging:
  a:
    page_cache_enabled: true

prod:
  a:
    page_cache_enabled: true

There are other app.yml settings available. You can adjust the cache lifetime (which defaults to 300 seconds), the Symfony cache class used (which defaults to sfFileCache), and the options passed to the cache class (which default to a reasonable setting for the cache folder name). Note that this means you can switch to sfAPCCache or or sfMemcacheCache if you prefer them:

all:
  a:
    page_cache_lifetime: 300
    page_cache_class: sfFileCache
    page_cache_options:
      cache_dir: /your/project/root/dir/data/a_writable/a_page_cache

One big gotcha: sometimes logged-out users do things that change content generated by GET requests. For instance, you might have code that implements blog comments for users without accounts. They add a comment via a POST request and you redirect them (GET request) to the blog post page they have already seen. You try to use a flash attribute to add a "thanks for your comment" message but it never shows up because the blog page is coming from the cache.

The solution is to set the aCacheInvalid flash attribute to true for the user in your "executeComment" action:

$this->getUser()->setFlashAttribute('aCacheInvalid', true);

This will prevent the cache from being consulted on the next request for this user.

You can also block the cache entirely for the remainder of a user's session with the regular setAttribute method:

$this->getUser()->setAttribute('aCacheInvalid', true);

The latter fix is better if you want the user to see the results of a change you have permitted them to make consistently as they move through previously visited pages on the site.

This is useful when you have users who are not really logged in according to Symfony (isAuthenticated() is false) but they are still doing interactive things that alter enough content that the cache becomes confusing for them.

Our advice is to do this generously whenever you think it's needed since the main purpose of the cache is to handle the bulk of casual, logged-out site visitors efficiently.

CSRF and Caching

CSRF protection for forms is a great feature of Symfony. But CSRF and caching are not friends. The CSRF token is unique for every user, and so when it is present our cache filter steps aside and lets that page be unique for that user... at the cost of generating it again for every user.

"But I don't have forms on every page, so that's OK," you say. Really? What about Apostrophe's login form, which is part of the layout? What about the language switcher form, which is also part of the layout?

As of 2011-06-22, these forms no longer use CSRF protection in the Apostrophe sandbox. But if you copied your site from our sandbox before that, you'll want to add this call to the configure method of aLanguageForm and sfGuardSigninForm at the project level (you do not have to modify files in the plugins, ever):

$this->disableCSRFProtection();

The language switcher form is harmless, and the signin form is useless to an attacker who doesn't already have your password, so neither of these really need CSRF. Please do not shut off CSRF for forms that could be used to trigger dangerous actions like deleting content or making purchases. In those situations CSRF protection is genuinely necessary. Consider using AJAX to load CSRF-protected forms on demand if they must be accessible from every page.

Caching areas with app_a_area_cache_enabled

This is a new option which can be used with the page cache or independently. When this option is set to true, each individual area (whether inserted by a_slot or a_area) is cached for 5 minutes for logged out users.

This is a big win for sites that use memory-hungry slots like the blog slot in sidebars that appear at many different URLs but present at least some redundant content.

This feature is suitable for use with all of our standard slots, none of which pay attention to anything other than the options passed to them (and the current contents of the database, of course, so it may take 5 minutes for a logged out user to see changes made by an editor).

If you have custom slots that look at URL parameters or other externalities this feature may not be for you. However note that it's OK to have links to ajax actions that care about such things from your normal view, as long as the initial state of the slot when the page is generated is not dependent on URL parameters.

This feature has no impact on sites that do not explicitly enable the flag. We do not enable it on our sandbox.

Adding Cloudflare

If one server is not enough even with the page cache in place you are far from stuck. In 2013 there are several companies that offer "CDN as a service": Content Delivery Networks that cache all your static files and, if you configure Apostrophe and the CDN correctly, all of your pages as well.

* To get started sign up for an appropriate account on cloudflare.com (the free plan should work, but choose according to your professional needs).

* You will need to change the DNS servers for your domain to point to cloudflare, per their instructions.

* You will need to make sure that all other hostnames for your site redirect to a name that DOES have a subdomain, usually: www.mydomain.com

* Again: you cannot use a bare domain name as the final hostname people see, you should redirect that to www.* at the Apache configuration level. This is due to a limitation of DNS that cloudflare is stuck with.

* Once your DNS is moved to cloudflare, make sure that you have a record for www.mydomain.com that DOES have cloudflare enabled (click the gray cloud to make it yellow), and a record for edit.mydomain.com that does NOT have cloudflare enabled. Point them both to the IP address of your own server (usually a VPS).

* Add a "page rule" in cloudflare matching everything: /* and set it to "cache everything." Set the TTL to just 30 minutes so that you can push edits to the world within a reasonable timeframe. (There is also a cache-clearing button in cloudflare.)

* Make sure your server's firewall, if any, is configured not to be upset by large amounts of traffic from cloudflare's IP address ranges.

* Always log in to edit via the edit.mydomain.com hostname, not www.mydomain.com. Be aware that users CAN NOT log in successfully on www.mydomain.com. If you need logged-in functionality "at scale," you'll need to keep reading.

* Configure Apostrophe to report cache headers correctly, following the next section. Although Cloudflare's "cache everything" option currently appears to ignore most caching-related headers, we shouldn't rely on that. Make sure your directives match your desires rather than the default Symfony "cache nothing" headers.

Sending Cache Headers

Once you have the page cache in place you can also enable sending cache headers so that individual browsers and, potentially, CDNs will cache the content correctly as well, further reducing load on your server.

You'll need to add these settings in app.yml, above and beyond what you did already:

prod:
  a:
    page_cache_set_headers: true
    page_cache_editing_host: edit.mydomain.com

Note that you must have an alternate server alias hostname for the site (not a redirect) that matches page_cache_editing_host so your users can edit by logging in at that name. This alternate host will support a logged-in experience. The main host will NOT support logging in when you use page_cache_set_headers.

You must also edit factories.yml to specify an alternate storage class that supports sessions if and only if the user is on the editing host. Otherwise caching is prevented by the starting of a session:

all:
  storage:
    class: aSessionStorageIfEditingHost
    param:
      session_name: symfony
      auto_start: true

Note that this class uses sfSessionStorage as a base class and behaves normally if page_cache_set_headers is not active for the current environment.

Now your server will send headers that match the caching behavior you want, for best compatibility with well-behaved CDNs that respect them (cloudflare may ignore them, so you may need to add page rules). And also individual browsers should cache the content as you specify (although most will still fetch it anew if you click refresh, so don't worry if you see that behavior in your tests).