Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Comment utiliser les meilleures pratiques d’HTTP pour mettre en cache votre site web - LaFéWeb 17

Ca901ddcea38854b9783781c91fc87c9?s=47 Thijs Feryn
November 16, 2017

Comment utiliser les meilleures pratiques d’HTTP pour mettre en cache votre site web - LaFéWeb 17

My very first presentation in French, delivered at a La FéWeb meetup in Louvain-la-Neuve.

More info: https://feryn.eu/speaking/comment-utiliser-les-meilleures-pratiques-dhttp-pour-mettre-en-cache-votre-site-web/

Ca901ddcea38854b9783781c91fc87c9?s=128

Thijs Feryn

November 16, 2017
Tweet

Transcript

  1. Comment utiliser les meilleures pratiques d’HTTP pour mettre en cache

    votre site web Thijs Feryn
  2. Slow websites suck

  3. Web performance is an essential part of the user experience

  4. Slow ~ Down

  5. None
  6. None
  7. None
  8. Identify slowest parts

  9. Reduce the impact of the code on the server

  10. Not just raw speed

  11. Scale

  12. Optimize

  13. After a while you hit the limits

  14. Cache

  15. Bonjour tout le monde

  16. Hi, I’m Thijs

  17. I’m @ThijsFeryn on Twitter

  18. I’m an Evangelist At

  19. I’m an Evangelist At

  20. None
  21. None
  22. Cache

  23. Cacher? Faire du “caching”? Mettre en cache?

  24. Don’t recompute if the data hasn’t changed

  25. None
  26. What if we could design our software with HTTP caching

    in mind?
  27. None
  28. Reverse caching proxy

  29. Normally User Server

  30. With ReCaPro * User ReCaPro Server * Reverse Caching Proxy

  31. Content Delivery Network

  32. None
  33. HTTP caching mechanisms Expires: Sat, 09 Sep 2017 14:30:00 GMT

    Cache-control: public, max-age=3600, s-maxage=86400 Cache-control: private, no-cache, no-store
  34. Common problems

  35. None
  36. None
  37. Time To Live

  38. Cache variations

  39. Authentication

  40. None
  41. ✓Multi-lingual (Accept-Language) ✓Nav ✓Header ✓Footer ✓Main ✓Login page & private

    content
  42. <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/html">
 <head>
 <title>{% block title %}{% endblock

    %} - Developing cacheable websites</title>
 <meta name="viewport" content="width=device-width, initial-scale=1.0” />
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 <link rel="stylesheet" href="/css/bootstrap.min.css" />
 <script src=“/js/jquery.min.js"></script>
 </head>
 <body>
 <div class="container-fluid">
 {{ include('header.twig') }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">
 {{ include('nav.twig') }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ include('footer.twig') }}
 </div>
 </body>
 </html> Base template
  43. None
  44. The mission Maximum Cacheability

  45. Cache-control

  46. None
  47. None
  48. Cache-Control: public, s-maxage=500

  49. Purging

  50. Conditional requests

  51. Only fetch payload that has changed

  52. HTTP/1.1 200 OK

  53. Otherwise: HTTP/1.1 304 Not Modified

  54. Conditional requests HTTP/1.1 200 OK Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27 Content-type:

    text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0
  55. Conditional requests HTTP/1.0 304 Not Modified Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27

    GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0 If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  56. Conditional requests HTTP/1.1 200 OK Host: localhost Last-Modified: Fri, 22

    Jul 2016 10:11:16 GMT Content-type: text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0
  57. Conditional requests HTTP/1.0 304 Not Modified Host: localhost Last-Modified: Fri,

    22 Jul 2016 10:11:16 GMT GET / HTTP/1.1 Host: localhost User-Agent: curl/7.48.0 If-Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
  58. Cache-Control: public, max-age=100, s-maxage=500, stale-while-revalidate=20

  59. Cache-Control: private, no-cache, no-store

  60. session cookie No cache

  61. None
  62. Code renders single HTTP response

  63. Lowest common denominator: no cache

  64. Block caching

  65. <esi:include src="/header" /> Edge Side Includes ✓Placeholder ✓Parsed by Varnish

    ✓Output is a composition of blocks ✓State per block ✓TTL per block
  66. Surrogate-Capability: key="ESI/1.0" Surrogate-Control: content="ESI/1.0" Varnish Backend <esi:include src="/header" /> Parse

    ESI placeholders Varnish
  67. ESI vs AJAX

  68. ✓ Server-side ✓ Standardized ✓ Processed on the “edge”, no

    in the browser ✓ Generally faster Edge-Side Includes - Sequential - One fails, all fail - Limited implementation in Varnish
  69. ✓ Client-side ✓ Common knowledge ✓ Parallel processing ✓ Graceful

    degradation AJAX - Processed by the browser - Extra roundtrips - Somewhat slower
  70. Subrequests

  71. <div class="container-fluid">
 {{ include('header.twig') }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">


    {{ include('nav.twig') }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ include('footer.twig') }}
 </div> <div class="container-fluid">
 {{ render_esi(url('header')) }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">
 {{ render_esi(url('nav')) }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ render_hinclude(url('footer')) }}
 </div>
  72. <div class="container-fluid"> <esi:include src="/header" /> <div class="row"> <div class="col-sm-3 col-lg-2">

    <esi:include src="/nav" /> </div> <div class="col-sm-9 col-lg-10"> <div class="page-header"> <h1>An example page <small>Rendered at 2017-05-17 16:57:14</small></h1> </div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero sollicitudin,…</p> </div> </div> <hx:include src="/footer"></hx:include> </div>
  73. Problem: no language cache variation

  74. Vary: Accept-Language

  75. None
  76. None
  77. None
  78. ✓Navigation page ✓Private page Weak spots Not cached because of

    stateful content
  79. Move state client-side

  80. Replace PHP session with JSON Web Tokens

  81. JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pb iIsImV4cCI6MTQ5NTUyODc1NiwibG9naW4iOnRydWV9.u4Idy- SYnrFdnH1h9_sNc4OasORBJcrh2fPo1EOTre8 ✓3 parts ✓Dot separated ✓Base64 encoded

    JSON ✓Header ✓Payload ✓Signature (HMAC with secret)
  82. eyJzdWIiOiJhZG1pbiIsIm V4cCI6MTQ5NTUyODc1Niwi bG9naW4iOnRydWV9 { "alg": "HS256", "typ": "JWT" } {

    "sub": "admin", "exp": 1495528756, "login": true } HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret ) eyJhbGciOiJIUzI1NiIsI nR5cCI6IkpXVCJ9 u4Idy- SYnrFdnH1h9_sNc4OasOR BJcrh2fPo1EOTre8
  83. JWT Cookie:token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJz dWIiOiJhZG1pbiIsImV4cCI6MTQ5NTUyODc1NiwibG9naW4iOnRydW V9.u4Idy-SYnrFdnH1h9_sNc4OasORBJcrh2fPo1EOTre8 ✓Stored in a cookie ✓Can be

    validated by Varnish ✓Payload can be processed by any language (e.g. Javascript)
  84. sub jwt { std.log("Ready to perform some JWT magic"); if(cookie.isset("jwt_cookie"))

    { #Extract header data from JWT var.set("token", cookie.get("jwt_cookie")); var.set("header", regsub(var.get("token"),"([^\.]+)\.[^\.]+\.[^\.]+","\1")); var.set("type", regsub(digest.base64url_decode(var.get("header")),{"^.*?"typ"\s*:\s*"(\w+)".*?$"},"\1")); var.set("algorithm", regsub(digest.base64url_decode(var.get("header")),{"^.*?"alg"\s*:\s*"(\w+)".*?$"},"\1")); #Don't allow invalid JWT header if(var.get("type") == "JWT" && var.get("algorithm") == "HS256") { #Extract signature & payload data from JWT var.set("rawPayload",regsub(var.get("token"),"[^\.]+\.([^\.]+)\.[^\.]+$","\1")); var.set("signature",regsub(var.get("token"),"^[^\.]+\.[^\.]+\.([^\.]+)$","\1")); var.set("currentSignature",digest.base64url_nopad_hex(digest.hmac_sha256(var.get("key"),var.get("header") + "." + var.get("rawPayload")))); var.set("payload", digest.base64url_decode(var.get("rawPayload"))); var.set("exp",regsub(var.get("payload"),{"^.*?"exp"\s*:\s*([0-9]+).*?$"},"\1")); var.set("jti",regsub(var.get("payload"),{"^.*?"jti"\s*:\s*"([a-z0-9A-Z_\-]+)".*?$"},"\1")); var.set("userId",regsub(var.get("payload"),{"^.*?"uid"\s*:\s*"([0-9]+)".*?$"},"\1")); var.set("roles",regsub(var.get("payload"),{"^.*?"roles"\s*:\s*"([a-z0-9A-Z_\-, ]+)".*?$"},"\1")); #Only allow valid userId if(var.get("userId") ~ "^\d+$") { #Don't allow expired JWT if(std.time(var.get("exp"),now) >= now) { #SessionId should match JTI value from JWT if(cookie.get(var.get("sessionCookie")) == var.get("jti")) { #Don't allow invalid JWT signature if(var.get("signature") == var.get("currentSignature")) { #The sweet spot set req.http.X-login="true"; } else { std.log("JWT: signature doesn't match. Received: " + var.get("signature") + ", expected: " + var.get("currentSignature")); } } else { std.log("JWT: session cookie doesn't match JTI." + var.get("sessionCookie") + ": " + cookie.get(var.get("sessionCookie")) + ", JTI:" + var.get("jti")); } } else { std.log("JWT: token has expired"); } } else { std.log("UserId '"+ var.get("userId") +"', is not numeric"); } } else { std.log("JWT: type is not JWT or algorithm is not HS256"); } std.log("JWT processing finished. UserId: " + var.get("userId") + ". X-Login: " + req.http.X-login); } #Look for full private content if(req.url ~ "/node/2" && req.url !~ "^/user/login") { if(req.http.X-login != "true") { return(synth(302,"/user/login?destination=" + req.url)); } } } Insert incomprehensible Varnish VCL code here …
  85. X-Login: true End result: X-Login: false

  86. Extra cache variation required

  87. Vary: Accept-Language, X-Login Content for logged-in & anonymous differs

  88. <script language="JavaScript">
 function getCookie(name) {
 var value = "; "

    + document.cookie;
 var parts = value.split("; " + name + "=");
 if (parts.length == 2) return parts.pop().split(";").shift();
 }
 function parseJwt (token) {
 var base64Url = token.split('.')[1];
 var base64 = base64Url.replace('-', '+').replace('_', '/');
 return JSON.parse(window.atob(base64));
 };
 $(document).ready(function(){
 if ($.cookie('token') != null ){
 var token = parseJwt($.cookie("token"));
 $("#usernameLabel").html(', ' + token.sub);
 }
 });
 </script> Parse JWT in Javascript
  89. Does not require backend access

  90. None
  91. None
  92. https://feryn.eu https://twitter.com/ThijsFeryn https://instagram.com/ThijsFeryn