SunshinePHP, Feb 8th 2013 © David Buchmann, Liip AG
                
David is a senior developper at Liip SA, specializing in Symfony2. He happens to also be a certified Scrum Master and sometimes enjoys doing the scrum master or product owner role for a project.
                Liip is doing custom web development with PHP in Switzerland.
Jordi Boggiano
Understand the effects of your changes.
Donald E. Knuth
# layout.html.twig
@@ -6,8 +6,6 @@
    {% include 'DbuCoreBundle::stylesheets.html.twig' %}
-   {% include 'DbuCoreBundle::javascripts.html.twig' %}
    <link rel="shortcut icon" href="{{ asset('favicon.ico') }}" >
  </head>
@@ -91,5 +89,11 @@
    </div>
+   {# javascript at the bottom: browser waits with rendering
+      until all files referenced before are loaded
+   #}
+   {% include 'DbuCoreBundle::javascripts.html.twig' %}
</body>
        
        Browser starts rendering after as soon as it has all CSS files, no need to wait for javascript to load.
Page renders after about 5 seconds
Browser starts rendering after as soon as it has all CSS files, no need to wait for javascript to load.
Page renders after about 5 seconds
/* main.css */
.sprite {
    display:block;
    background-repeat:no-repeat;
    background-image:url(/assets/images/sprite.png);
}
.logo {
    width:336px;
    height:63px;
}
/* generated sprites.css */
.logo {background-position:-160px 0px}
        
        
# config_prod.yml
assetic:
  filters:
    yui_css:
      jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
      apply_to: "\.css$"
    yui_js:
      jar: "%kernel.root_dir%/Resources/java/yuicompressor.jar"
      apply_to: "\.js$"
            
            YUICompressor has been discontinued by the YUI team. You can still get the compiled .jar from the github page.
Server: Apache/2.2.16 (Ubuntu)
cache-control: max-age=600, public
last-modified: Wed, 02 May 2012 09:12:17 GMT
            
            
If-Modified-Since: Wed, 02 May 2012 09:12:17 GMT
Server:
304 Not Modified
            
            
firewalls:
  name:
    logout:
      path:   /logout
      target: /
      invalidate_session: true
      delete_cookies:
        PHPSESSID: { path: null, domain: null }
            
        
ExpiresActive On
ExpiresByType text/css  "access plus 3 months"
ExpiresByType text/javascript "access plus 3 months"
ExpiresByType application/javascript "access plus 3 months"
ExpiresByType image/gif "access plus 1 day"
ExpiresByType image/png "access plus 1 day"
ExpiresByType image/jpg "access plus 1 day"
ExpiresByType image/jpeg "access plus 1 day"
            
        
# app/config.yml
framework:
  templating:
    assets_version: v1
            
            
<!-- generated HTML -->
<link rel="stylesheet" href="/css/style.css?v1" type="text/css"/>
...
<script src="/js/scripts.js?v1"></script>
            
        
// DefaultController::indexAction
$response = $this->render('DbuCoreBundle:Default:index.html.twig', array());
$response->setMaxAge(600);
$response->setPublic();
return $response;
            
            Add such code to every action that renders a response.
Or annotate every action with a @Cache comment.
# config.yml
liip_cache_control:
    rules:
        # Make sure login and logout are not cached at all
        -
            path: ^/(login|login_check|logout)
            controls:
                private: true
                max_age: 0
        # Cache the homepage for 10 minutes
        -
            path: ^/$
            vary: Cookie
            controls:
                public: true
                max_age: 600
        # Cache every other page for 1 hour
        -
            path: ^/
            vary: Cookie
            controls:
                public: true
                max_age: 3600
            
            Clear the Symfony cache whenever you change values here
        Think carefully and test thoroughly
# app/config.yml
liip_cache_control:
  varnish:
    domain: http://performance.lo
    ips: 10.0.0.10, 10.0.0.11 # comma separated list of ips, or an array of ips
    port: 80  # port Varnish is listening on for incoming web connections
            
            
// CommentsController::postAction
// the page changed, send a purge request for this url
...
$varnish = $this->container->get('liip_cache_control.varnish');
$varnish->invalidatePath($this->generateUrl('home'));
...
            
            
# varnish.vcl
sub vcl_recv {
    if (req.request == "PURGE") {
        # should check if allowed, i.e. IP filter
        purge("req.url ~ " req.url);
        error 200 "Success";
    }
    ...
            
        
# app/config/config.yml
framework:
  esi: { enabled: true }
# app/config/routing.yml
_internal:
  resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
  prefix:   /_internal
            
            Make sure either your webserver is only reachable by Varnish or add access restriction to the Varnish server IP for /_internal
            
index.html.twig
-   {% render 'DbuCoreBundle:Comments:comments' %}
+   {% render 'DbuCoreBundle:Comments:comments' with {}, {'standalone': true} %}
layout.html.twig
    <li class=login>
-       {% render 'DbuCoreBundle:User:showLoginBox' %}
+       {% render 'DbuCoreBundle:User:showLoginBox' with {}, {'standalone': true} %}
    </li>
            
        
// UserController::showLoginBoxAction
$response = $this->render('DbuCoreBundle:User:loginBox.html.twig', array());
$response->setVary('Cookie', false);
$response->setMaxAge(0);
$response->setPrivate();
return $response;
            
        
// CommentsController::commentsAction
$response = $this->render(
    'DbuCoreBundle:Comments:comments.html.twig',
    array('comments' => $this->getComments())
);
$response->setMaxAge(3600);
$response->setPublic();
return $response;
            
            
$varnish = $this->container->get('liip_cache_control.varnish');
$kernel = $this->container->get('http_kernel');
$varnish->invalidatePath(
    $kernel->generateInternalUri('DbuCoreBundle:Comments:comments')
);
            
            Note: generateInternalUri is helpful but does not check if controller exists
                david# app/console cache:clear --env=prod --no-debug
                david# curl -H "Surrogate-Capability: abc=ESI/1.0" http://performance.lo/
            
            
...
<esi:include src="/_internal/secure/DbuCoreBundle%3AUser%3AshowLoginBox/none.html" onerror="continue" />
...
<esi:include src="/_internal/secure/DbuCoreBundle%3AComments%3Acomments/none.html" onerror="continue" />
...
            
        
sub vcl_recv {
    set req.http.Surrogate-Capability = "abc=ESI/1.0";
    ...
    # try to lookup even if there is a cookie
    return (lookup);
}
            
            
sub vcl_fetch {
    if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
        unset beresp.http.Surrogate-Control;
        set beresp.do_esi = true; # just "esi;" for varnish < 3.0
    }
    ...
    if (beresp.http.Vary) {
        return (pass);
    }
}
            
            
sub vcl_deliver {
    if (! req.url ~ ".*\.(css|js|png|gif|jpg|ttf)(\?.*)?$") {
        set resp.http.Vary = "Cookie";
        # if-modified-since will only confuse us, remove date
        unset resp.http.Last-Modified;
    }
}
            
        Mix different input formats:
{% stylesheets output="assets/style.css"
    'path/to/Resources/css/yahoo-reset.css'
    'path/to/Resources/less/main.less'
    'path/to/Resources/less/misc.less'
%}
  <link href="{{ asset_url }}" media="screen"
    type="text/css" rel="stylesheet" />
{% endstylesheets %}