A place to cache linked articles (think custom and personal wayback machine)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.html 39KB

4 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. <!doctype html><!-- This is a valid HTML5 document. -->
  2. <!-- Screen readers, SEO, extensions and so on. -->
  3. <html lang=fr>
  4. <!-- Has to be within the first 1024 bytes, hence before the <title>
  5. See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
  6. <meta charset=utf-8>
  7. <!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
  8. <!-- The viewport meta is quite crowded and we are responsible for that.
  9. See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
  10. <meta name=viewport content="width=device-width,minimum-scale=1,initial-scale=1,shrink-to-fit=no">
  11. <!-- Required to make a valid HTML5 document. -->
  12. <title>Async Fragments: Rediscovering Progressive HTML Rendering with Marko (archive) — David Larlet</title>
  13. <!-- Generated from https://realfavicongenerator.net/ such a mess. -->
  14. <link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons/apple-touch-icon.png">
  15. <link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons/favicon-32x32.png">
  16. <link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons/favicon-16x16.png">
  17. <link rel="manifest" href="/manifest.json">
  18. <link rel="mask-icon" href="/static/david/icons/safari-pinned-tab.svg" color="#5bbad5">
  19. <link rel="shortcut icon" href="/static/david/icons/favicon.ico">
  20. <meta name="apple-mobile-web-app-title" content="David Larlet">
  21. <meta name="application-name" content="David Larlet">
  22. <meta name="msapplication-TileColor" content="#da532c">
  23. <meta name="msapplication-config" content="/static/david/icons/browserconfig.xml">
  24. <meta name="theme-color" content="#f0f0ea">
  25. <!-- That good ol' feed, subscribe :p. -->
  26. <link rel=alternate type="application/atom+xml" title=Feed href="/david/log/">
  27. <meta name="robots" content="noindex, nofollow">
  28. <meta content="origin-when-cross-origin" name="referrer">
  29. <!-- Canonical URL for SEO purposes -->
  30. <link rel="canonical" href="http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/">
  31. <style>
  32. /* http://meyerweb.com/eric/tools/css/reset/ */
  33. html, body, div, span,
  34. h1, h2, h3, h4, h5, h6, p, blockquote, pre,
  35. a, abbr, address, big, cite, code,
  36. del, dfn, em, img, ins,
  37. small, strike, strong, tt, var,
  38. dl, dt, dd, ol, ul, li,
  39. fieldset, form, label, legend,
  40. table, caption, tbody, tfoot, thead, tr, th, td,
  41. article, aside, canvas, details, embed,
  42. figure, figcaption, footer, header, hgroup,
  43. menu, nav, output, ruby, section, summary,
  44. time, mark, audio, video {
  45. margin: 0;
  46. padding: 0;
  47. border: 0;
  48. font-size: 100%;
  49. font: inherit;
  50. vertical-align: baseline;
  51. }
  52. /* HTML5 display-role reset for older browsers */
  53. article, aside, details, figcaption, figure,
  54. footer, header, hgroup, menu, nav, section { display: block; }
  55. body { line-height: 1; }
  56. blockquote, q { quotes: none; }
  57. blockquote:before, blockquote:after,
  58. q:before, q:after {
  59. content: '';
  60. content: none;
  61. }
  62. table {
  63. border-collapse: collapse;
  64. border-spacing: 0;
  65. }
  66. /* http://practicaltypography.com/equity.html */
  67. /* https://calendar.perfplanet.com/2016/no-font-face-bulletproof-syntax/ */
  68. /* https://www.filamentgroup.com/lab/js-web-fonts.html */
  69. @font-face {
  70. font-family: 'EquityTextB';
  71. src: url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff2') format('woff2'),
  72. url('/static/david/css/fonts/Equity-Text-B-Regular-webfont.woff') format('woff');
  73. font-weight: 300;
  74. font-style: normal;
  75. font-display: swap;
  76. }
  77. @font-face {
  78. font-family: 'EquityTextB';
  79. src: url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff2') format('woff2'),
  80. url('/static/david/css/fonts/Equity-Text-B-Italic-webfont.woff') format('woff');
  81. font-weight: 300;
  82. font-style: italic;
  83. font-display: swap;
  84. }
  85. @font-face {
  86. font-family: 'EquityTextB';
  87. src: url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff2') format('woff2'),
  88. url('/static/david/css/fonts/Equity-Text-B-Bold-webfont.woff') format('woff');
  89. font-weight: 700;
  90. font-style: normal;
  91. font-display: swap;
  92. }
  93. @font-face {
  94. font-family: 'ConcourseT3';
  95. src: url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff2') format('woff2'),
  96. url('/static/david/css/fonts/concourse_t3_regular-webfont-20190806.woff') format('woff');
  97. font-weight: 300;
  98. font-style: normal;
  99. font-display: swap;
  100. }
  101. /* http://practice.typekit.com/lesson/caring-about-opentype-features/ */
  102. body {
  103. /* http://www.cssfontstack.com/ Palatino 99% Win 86% Mac */
  104. font-family: "EquityTextB", Palatino, serif;
  105. background-color: #f0f0ea;
  106. color: #07486c;
  107. font-kerning: normal;
  108. -moz-osx-font-smoothing: grayscale;
  109. -webkit-font-smoothing: subpixel-antialiased;
  110. text-rendering: optimizeLegibility;
  111. font-variant-ligatures: common-ligatures contextual;
  112. font-feature-settings: "kern", "liga", "clig", "calt";
  113. }
  114. pre, code, kbd, samp, var, tt {
  115. font-family: 'TriplicateT4c', monospace;
  116. }
  117. em {
  118. font-style: italic;
  119. color: #323a45;
  120. }
  121. strong {
  122. font-weight: bold;
  123. color: black;
  124. }
  125. nav {
  126. background-color: #323a45;
  127. color: #f0f0ea;
  128. display: flex;
  129. justify-content: space-around;
  130. padding: 1rem .5rem;
  131. }
  132. nav:last-child {
  133. border-bottom: 1vh solid #2d7474;
  134. }
  135. nav a {
  136. color: #f0f0ea;
  137. }
  138. nav abbr {
  139. border-bottom: 1px dotted white;
  140. }
  141. h1 {
  142. border-top: 1vh solid #2d7474;
  143. border-bottom: .2vh dotted #2d7474;
  144. background-color: #e3e1e1;
  145. color: #323a45;
  146. text-align: center;
  147. padding: 5rem 0 4rem 0;
  148. width: 100%;
  149. font-family: 'ConcourseT3';
  150. display: flex;
  151. flex-direction: column;
  152. }
  153. h1.single {
  154. padding-bottom: 10rem;
  155. }
  156. h1 span {
  157. position: absolute;
  158. top: 1vh;
  159. left: 20%;
  160. line-height: 0;
  161. }
  162. h1 span a {
  163. line-height: 1.7;
  164. padding: 1rem 1.2rem .6rem 1.2rem;
  165. border-radius: 0 0 6% 6%;
  166. background: #2d7474;
  167. font-size: 1.3rem;
  168. color: white;
  169. text-decoration: none;
  170. }
  171. h2 {
  172. margin: 4rem 0 1rem;
  173. border-top: .2vh solid #2d7474;
  174. padding-top: 1vh;
  175. }
  176. h3 {
  177. text-align: center;
  178. margin: 3rem 0 .75em;
  179. }
  180. hr {
  181. height: .4rem;
  182. width: .4rem;
  183. border-radius: .4rem;
  184. background: #07486c;
  185. margin: 2.5rem auto;
  186. }
  187. time {
  188. display: bloc;
  189. margin-left: 0 !important;
  190. }
  191. ul, ol {
  192. margin: 2rem;
  193. }
  194. ul {
  195. list-style-type: square;
  196. }
  197. a {
  198. text-decoration-skip-ink: auto;
  199. text-decoration-thickness: 0.05em;
  200. text-underline-offset: 0.09em;
  201. }
  202. article {
  203. max-width: 50rem;
  204. display: flex;
  205. flex-direction: column;
  206. margin: 2rem auto;
  207. }
  208. article.single {
  209. border-top: .2vh dotted #2d7474;
  210. margin: -6rem auto 1rem auto;
  211. background: #f0f0ea;
  212. padding: 2rem;
  213. }
  214. article p:last-child {
  215. margin-bottom: 1rem;
  216. }
  217. p {
  218. padding: 0 .5rem;
  219. margin-left: 3rem;
  220. }
  221. p + p,
  222. figure + p {
  223. margin-top: 2rem;
  224. }
  225. blockquote {
  226. background-color: #e3e1e1;
  227. border-left: .5vw solid #2d7474;
  228. display: flex;
  229. flex-direction: column;
  230. align-items: center;
  231. padding: 1rem;
  232. margin: 1.5rem;
  233. }
  234. blockquote cite {
  235. font-style: italic;
  236. }
  237. blockquote p {
  238. margin-left: 0;
  239. }
  240. figure {
  241. border-top: .2vh solid #2d7474;
  242. background-color: #e3e1e1;
  243. text-align: center;
  244. padding: 1.5rem 0;
  245. margin: 1rem 0 0;
  246. font-size: 1.5rem;
  247. width: 100%;
  248. }
  249. figure img {
  250. max-width: 250px;
  251. max-height: 250px;
  252. border: .5vw solid #323a45;
  253. padding: 1px;
  254. }
  255. figcaption {
  256. padding: 1rem;
  257. line-height: 1.4;
  258. }
  259. aside {
  260. display: flex;
  261. flex-direction: column;
  262. background-color: #e3e1e1;
  263. padding: 1rem 0;
  264. border-bottom: .2vh solid #07486c;
  265. }
  266. aside p {
  267. max-width: 50rem;
  268. margin: 0 auto;
  269. }
  270. /* https://fvsch.com/code/css-locks/ */
  271. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  272. font-size: 1rem;
  273. line-height: calc( 1.5em + 0.2 * 1rem );
  274. }
  275. h1 {
  276. font-size: 1.9rem;
  277. line-height: calc( 1.2em + 0.2 * 1rem );
  278. }
  279. h2 {
  280. font-size: 1.6rem;
  281. line-height: calc( 1.3em + 0.2 * 1rem );
  282. }
  283. h3 {
  284. font-size: 1.35rem;
  285. line-height: calc( 1.4em + 0.2 * 1rem );
  286. }
  287. @media (min-width: 20em) {
  288. /* The (100vw - 20rem) / (50 - 20) part
  289. resolves to 0-1rem, depending on the
  290. viewport width (between 20em and 50em). */
  291. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  292. font-size: calc( 1rem + .6 * (100vw - 20rem) / (50 - 20) );
  293. line-height: calc( 1.5em + 0.2 * (100vw - 50rem) / (20 - 50) );
  294. margin-left: 0;
  295. }
  296. h1 {
  297. font-size: calc( 1.9rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  298. line-height: calc( 1.2em + 0.2 * (100vw - 50rem) / (20 - 50) );
  299. }
  300. h2 {
  301. font-size: calc( 1.5rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  302. line-height: calc( 1.3em + 0.2 * (100vw - 50rem) / (20 - 50) );
  303. }
  304. h3 {
  305. font-size: calc( 1.35rem + 1.5 * (100vw - 20rem) / (50 - 20) );
  306. line-height: calc( 1.4em + 0.2 * (100vw - 50rem) / (20 - 50) );
  307. }
  308. }
  309. @media (min-width: 50em) {
  310. /* The right part of the addition *must* be a
  311. rem value. In this example we *could* change
  312. the whole declaration to font-size:2.5rem,
  313. but if our baseline value was not expressed
  314. in rem we would have to use calc. */
  315. p, li, pre, code, kbd, samp, var, tt, time, details, figcaption {
  316. font-size: calc( 1rem + .6 * 1rem );
  317. line-height: 1.5em;
  318. }
  319. p, li, pre, details {
  320. margin-left: 3rem;
  321. }
  322. h1 {
  323. font-size: calc( 1.9rem + 1.5 * 1rem );
  324. line-height: 1.2em;
  325. }
  326. h2 {
  327. font-size: calc( 1.5rem + 1.5 * 1rem );
  328. line-height: 1.3em;
  329. }
  330. h3 {
  331. font-size: calc( 1.35rem + 1.5 * 1rem );
  332. line-height: 1.4em;
  333. }
  334. figure img {
  335. max-width: 500px;
  336. max-height: 500px;
  337. }
  338. }
  339. figure.unsquared {
  340. margin-bottom: 1.5rem;
  341. }
  342. figure.unsquared img {
  343. height: inherit;
  344. }
  345. @media print {
  346. body { font-size: 100%; }
  347. a:after { content: " (" attr(href) ")"; }
  348. a, a:link, a:visited, a:after {
  349. text-decoration: underline;
  350. text-shadow: none !important;
  351. background-image: none !important;
  352. background: white;
  353. color: black;
  354. }
  355. abbr[title] { border-bottom: 0; }
  356. abbr[title]:after { content: " (" attr(title) ")"; }
  357. img { page-break-inside: avoid; }
  358. @page { margin: 2cm .5cm; }
  359. h1, h2, h3 { page-break-after: avoid; }
  360. p3 { orphans: 3; widows: 3; }
  361. img {
  362. max-width: 250px !important;
  363. max-height: 250px !important;
  364. }
  365. nav, aside { display: none; }
  366. }
  367. ul.with_columns {
  368. column-count: 1;
  369. }
  370. @media (min-width: 20em) {
  371. ul.with_columns {
  372. column-count: 2;
  373. }
  374. }
  375. @media (min-width: 50em) {
  376. ul.with_columns {
  377. column-count: 3;
  378. }
  379. }
  380. ul.with_two_columns {
  381. column-count: 1;
  382. }
  383. @media (min-width: 20em) {
  384. ul.with_two_columns {
  385. column-count: 1;
  386. }
  387. }
  388. @media (min-width: 50em) {
  389. ul.with_two_columns {
  390. column-count: 2;
  391. }
  392. }
  393. .gallery {
  394. display: flex;
  395. flex-wrap: wrap;
  396. justify-content: space-around;
  397. }
  398. .gallery figure img {
  399. margin-left: 1rem;
  400. margin-right: 1rem;
  401. }
  402. .gallery figure figcaption {
  403. font-family: 'ConcourseT3'
  404. }
  405. footer {
  406. font-family: 'ConcourseT3';
  407. display: flex;
  408. flex-direction: column;
  409. border-top: 3px solid white;
  410. padding: 4rem 0;
  411. background-color: #07486c;
  412. color: white;
  413. }
  414. footer > * {
  415. max-width: 50rem;
  416. margin: 0 auto;
  417. }
  418. footer a {
  419. color: #f1c40f;
  420. }
  421. footer .avatar {
  422. width: 200px;
  423. height: 200px;
  424. border-radius: 50%;
  425. float: left;
  426. -webkit-shape-outside: circle();
  427. shape-outside: circle();
  428. margin-right: 2rem;
  429. padding: 2px 5px 5px 2px;
  430. background: white;
  431. border-left: 1px solid #f1c40f;
  432. border-top: 1px solid #f1c40f;
  433. border-right: 5px solid #f1c40f;
  434. border-bottom: 5px solid #f1c40f;
  435. }
  436. </style>
  437. <h1>
  438. <span><a id="jumper" href="#jumpto" title="Un peu perdu ?">?</a></span>
  439. Async Fragments: Rediscovering Progressive HTML Rendering with Marko (archive)
  440. <time>Pour la pérennité des contenus liés. Non-indexé, retrait sur simple email.</time>
  441. </h1>
  442. <section>
  443. <article>
  444. <h3><a href="http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/">Source originale du contenu</a></h3>
  445. <p>At eBay, we take site speed very seriously and are always looking for ways to allow developers to create faster-loading web apps. This involves fully understanding and controlling how web pages are delivered to web browsers. Progressive HTML rendering is a relatively old technique that can be used to improve the performance of websites, but it has been lost in a whole new class of web applications. The idea is simple: give the web browser a head start in downloading and rendering the page by flushing out early and multiple times. Browsers have always had the helpful feature of parsing and responding to the HTML as it is being streamed down from the server (even before the response is ended). This feature allows the HTML and external resources to be downloaded earlier, and for parts of the page to be rendered earlier. As a result, both the actual load time and the perceived load time improve.</p>
  446. <p>In this blog post, we will take an in-depth look at a technique we call “Async Fragments” that takes advantage of progressive HTML rendering to improve site speed in ways that do not drastically complicate how web applications are built. For concrete examples we will be using <a href="http://nodejs.org/">Node.js</a>, <a href="http://expressjs.com/">Express.js</a> and the <a href="https://github.com/raptorjs/marko">Marko templating engine</a> (a JavaScript templating engine that supports streaming, flushing, and asynchronous rendering). Even if you are not using these technologies, this post can give you insight into how your stack of choice could be further optimized.</p>
  447. <p>To see the techniques discussed in this post in action, please take a look at the <a href="https://marko-progressive-rendering.herokuapp.com/">accompanying sample application</a>.</p>
  448. <h2>Background</h2>
  449. <p>Progressive HTML rendering is discussed in the post <a href="http://blog.codinghorror.com/the-lost-art-of-progressive-html-rendering/">The Lost Art of Progressive HTML Rendering</a> by Jeff Atwood, which was published back in 2005. In addition, the “Flush the Buffer Early” rule is described by the Yahoo! Performance team in their <a href="https://developer.yahoo.com/performance/rules.html#flush">Best Practices for Speeding Up Your Web Site</a> guide. Stoyan Stefanov provides an in-depth look at progressive HTML rendering in his <a href="http://www.phpied.com/progressive-rendering-via-multiple-flushes/">Progressive rendering via multiple flushes</a> post. Facebook discussed how they use a technique they call <a href="https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919">“BigPipe”</a> to improve page load times and perceived performance by dividing up a page into “pagelets.” Those articles and techniques inspired many of the ideas discussed in this post.</p>
  450. <p>In the Node.js world, its most popular web framework, <a href="http://expressjs.com/">Express.js</a>, unfortunately recommends a view rendering engine that does not allow streaming and thus prevents progressive HTML rendering. In a recent post, <a href="http://strongloop.com/strongblog/bypassing-express-view-rendering-for-speed-and-modularity/">Bypassing Express View Rendering for Speed and Modularity</a>, I described how streaming can be achieved with Express.js; this post is largely a follow-up to discuss how progressive HTML rendering can be achieved with Node.js (with or without Express.js).</p>
  451. <h2>Without progressive HTML rendering</h2>
  452. <p>A page that does not utilize progressive HTML rendering will have a slower load time because the bytes will not be flushed out until the complete HTML response is built. In addition, after the client finally receives the <em>complete</em> HTML it will then see that it needs to download additional external resources (such as CSS, JavaScript, fonts, and images), and downloading these external resources will require additional round trips. In addition, pages that do not utilize progressive HTML rendering will also have a slower perceived load time, since the screen will not update until the complete HTML is downloaded <em>and</em> the CSS and fonts referenced in the <code>&lt;head&gt;</code> section are downloaded. Without progressive HTML rendering, a server/client waterfall chart might be similar to the following:</p>
  453. <p><img src="https://docs.google.com/uc?authuser=0&amp;id=0B68ucl6PKE9yV0pfOUZHRXVuTlU&amp;export=download" alt="Single Flush Waterfall Chart"/></p>
  454. <p>The corresponding page controller might look something like this:</p>
  455. <pre>function controller(req, res) {&#13;
  456. async.parallel([&#13;
  457. function loadSearchResults(callback) {&#13;
  458. ...&#13;
  459. },&#13;
  460. function loadFilters(callback) {&#13;
  461. ...&#13;
  462. },&#13;
  463. function loadAds(callback) {&#13;
  464. ...&#13;
  465. }&#13;
  466. ],&#13;
  467. function() {&#13;
  468. ...&#13;
  469. var viewModel = { ... };&#13;
  470. res.render('search', viewModel);&#13;
  471. })&#13;
  472. }</pre>
  473. <p>As you can see in the above code, the page HTML is not rendered until <em>all</em> of the data is asynchronously loaded.</p>
  474. <p>Because the HTML is not flushed until all back-end services are completed, the user will be staring at a blank screen for a large portion of the time. This will result in a sub-par user experience (especially with a poor network connection or with slow back-end services). We can do much better if we flush part of the HTML earlier.</p>
  475. <h2>Flushing the head early</h2>
  476. <p>A simple trick to improve the responsiveness of a website is to flush the head section immediately. The head section will typically include the links to the external CSS resources (i.e. the <code>&lt;link&gt;</code> tags), as well as the page header and navigation. With this approach the external CSS will be downloaded sooner and the initial page will be painted much sooner as shown in the following waterfall chart:</p>
  477. <p><img src="https://docs.google.com/uc?authuser=0&amp;id=0B68ucl6PKE9yTWttODlfWVR5N3M&amp;export=download" alt="Flush Head Waterfall Chart"/></p>
  478. <p>As you can see in the chart above, flushing the head early reduces the time to render the initial page. This technique improves the responsiveness of the page, but it does not significantly reduce the total time it takes to make the page fully functional. With this approach, the server is still waiting for all back-end services to complete before flushing the final HTML. In addition, downloading of external JavaScript resources will be delayed since <code>&lt;script&gt;</code> tags are placed at the end of the page (assuming you are following best practices) and don’t get sent out until the second and final flush.</p>
  479. <h2>Multiple flushes</h2>
  480. <p>Instead of flushing only the head early, it is often beneficial to flush multiple times before ending the response. Typically, a page can be divided into multiple fragments where some of the fragments may depend on data asynchronously loaded from various back-end services while others may not depend on any asynchronously loaded data. The fragments that depend on asynchronously loaded data should be rendered asynchronously and flushed as soon as possible.</p>
  481. <p>For now, we will assume that these fragments need to be flushed in the proper HTML order (versus the order that the data asynchronously loads), but we will also show how out-of-order flushing can be used to further improve both page load times and perceived performance. When using “in-order” flushing, fragments that complete out of order will need to be buffered until they are ready to be flushed in the proper order.</p>
  482. <h3>In-order flushing of async fragments</h3>
  483. <p>As an example, let’s assume we have divided a complex page into the following fragments:</p>
  484. <p><img src="https://docs.google.com/uc?authuser=0&amp;id=0B68ucl6PKE9yUnI2UU9LQWluS28&amp;export=download" alt="Page diagram"/></p>
  485. <p>Each fragment is assigned a number based on the order that it appears in the HTML document. In code, our output HTML for the page might look like the following:</p>
  486. <pre>&lt;html&gt;&#13;
  487. &lt;head&gt;&#13;
  488. &lt;title&gt;Clothing Store&lt;/title&gt;&#13;
  489. &lt;!-- 1a) Head &lt;link&gt; tags --&gt;&#13;
  490. &lt;/head&gt;&#13;
  491. &lt;body&gt;&#13;
  492. &lt;header&gt;&#13;
  493. &lt;!-- 1b) Header --&gt;&#13;
  494. &lt;/header&gt;&#13;
  495. &lt;div class="body"&gt;&#13;
  496. &lt;main&gt;&#13;
  497. &lt;!-- 2) Search Results --&gt;&#13;
  498. &lt;/main&gt;&#13;
  499. &lt;section class="filters"&gt;&#13;
  500. &lt;!-- 3) Search filters --&gt;&#13;
  501. &lt;/section&gt;&#13;
  502. &lt;section class="ads"&gt;&#13;
  503. &lt;!-- 4) Ads --&gt;&#13;
  504. &lt;/section&gt;&#13;
  505. &lt;/div&gt;&#13;
  506. &lt;footer&gt;&#13;
  507. &lt;!-- 5a) Footer --&gt;&#13;
  508. &lt;/footer&gt;&#13;
  509. &lt;!-- 5b) Body &lt;script&gt; tags --&gt;&#13;
  510. &lt;/body&gt;&#13;
  511. &lt;/html&gt;</pre>
  512. <p>The Marko templating engine provides a way to declaratively bind template fragments to asynchronous data provider functions (or <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">Promises</a>). An asynchronous fragment is rendered when the asynchronous data provider function invokes the provided callback with the data. If the asynchronous fragment is ready to be flushed, then it is immediately flushed to the output stream. Otherwise, if the asynchronous fragment completed out of order then the rendered HTML is buffered in memory until it is ready to be flushed. The Marko templating engine ensures that fragments are flushed in the proper order.</p>
  513. <p>Continuing with the previous example, our HTML page template with asynchronous fragments defined will be similar to the following:</p>
  514. <pre>&lt;html&gt;&#13;
  515. &lt;head&gt;&#13;
  516. &lt;title&gt;Clothing Store&lt;/title&gt;&#13;
  517. &lt;!-- Head &lt;link&gt; tags --&gt;&#13;
  518. &lt;/head&gt;&#13;
  519. &lt;body&gt;&#13;
  520. &lt;header&gt;&#13;
  521. &lt;!-- Header --&gt;&#13;
  522. &lt;/header&gt;&#13;
  523. &lt;div class="body"&gt;&#13;
  524. &lt;main&gt;&#13;
  525. &lt;!-- Search Results --&gt;&#13;
  526. &lt;async-fragment data-provider="data.searchResultsProvider"&#13;
  527. var="searchResults"&gt;&#13;
  528. &#13;
  529. &lt;!-- Do something with the search results data... --&gt;&#13;
  530. &lt;ul&gt;&#13;
  531. &lt;li for="item in searchResults.items"&gt;&#13;
  532. $item.title&#13;
  533. &lt;/li&gt;&#13;
  534. &lt;/ul&gt;&#13;
  535. &#13;
  536. &lt;/async-fragment&gt;&#13;
  537. &lt;/main&gt;&#13;
  538. &lt;section class="filters"&gt;&#13;
  539. &#13;
  540. &lt;!-- Search filters --&gt;&#13;
  541. &lt;async-fragment data-provider="data.filtersProvider"&#13;
  542. var="filters"&gt;&#13;
  543. &lt;!-- Do something with the filters data... --&gt;&#13;
  544. &lt;/async-fragment&gt;&#13;
  545. &#13;
  546. &lt;/section&gt;&#13;
  547. &lt;section class="ads"&gt;&#13;
  548. &#13;
  549. &lt;!-- Ads --&gt;&#13;
  550. &lt;async-fragment data-provider="data.adsProvider"&#13;
  551. var="ads"&gt;&#13;
  552. &lt;!-- Do something with the ads data... --&gt;&#13;
  553. &lt;/async-fragment&gt;&#13;
  554. &#13;
  555. &lt;/section&gt;&#13;
  556. &lt;/div&gt;&#13;
  557. &lt;footer&gt;&#13;
  558. &lt;!-- Footer --&gt;&#13;
  559. &lt;/footer&gt;&#13;
  560. &lt;!-- Body &lt;script&gt; tags --&gt;&#13;
  561. &lt;/body&gt;&#13;
  562. &lt;/html&gt;&#13;
  563. </pre>
  564. <p>The data provider functions should be passed to the template as part of the view model as shown in the following code for a sample page controller:</p>
  565. <pre>function controller(req, res) {&#13;
  566. template.render({&#13;
  567. searchResultsProvider: function(callback) {&#13;
  568. performSearch(req.params.category, callback);&#13;
  569. },&#13;
  570. &#13;
  571. filtersProvider: function(callback) {&#13;
  572. ...&#13;
  573. },&#13;
  574. &#13;
  575. adsProvider: function(callback) {&#13;
  576. ...&#13;
  577. }&#13;
  578. },&#13;
  579. res /* Render directly to the output HTTP response stream */);&#13;
  580. }</pre>
  581. <p>In this particular example, the “search results” async fragment appears first in the HTML template, and it happens to take the longest time to complete. As a result, all of the subsequent fragments will need to be buffered on the server. The resulting waterfall with in-order flushing of async fragments is shown below:</p>
  582. <p><img src="https://docs.google.com/uc?authuser=0&amp;id=0B68ucl6PKE9yZ0RTLWtINVlobHc&amp;export=download" alt="In-order Flush Waterfall Chart"/></p>
  583. <p>While the performance of this approach might be fine, we can enable out-of-order flushing for further performance gains as described in the next section.</p>
  584. <h3>Out-of-order flushing of async fragments</h3>
  585. <p>Marko achieves out-of-order flushing of async fragments by doing the following:</p>
  586. <p>Instead of waiting for an async fragment to finish, a placeholder HTML element with an assigned <code>id</code> is written to the output stream. Out-of-order async fragments are rendered before the ending <code>&lt;body&gt;</code> tag in the order that they complete. Each out-of-order async fragment is rendered into a hidden <code>&lt;div&gt;</code> element. Immediately after the out-of-order fragment, a <code>&lt;script&gt;</code> block is rendered to replace the placeholder DOM node with the DOM nodes of the corresponding out-of-order fragment. When all of the out-of-order async fragments complete, the remaining HTML (e.g. <code>&lt;/body&gt;&lt;/html&gt;</code>) will be flushed and the response ended.</p>
  587. <p>To clarify, here is what the output HTML might look like for a page with out-of-order flushing enabled:</p>
  588. <pre>&lt;html&gt;&#13;
  589. &lt;head&gt;&#13;
  590. &lt;title&gt;Clothing Store&lt;/title&gt;&#13;
  591. &lt;!-- 1a) Head &lt;link&gt; tags --&gt;&#13;
  592. &lt;/head&gt;&#13;
  593. &lt;body&gt;&#13;
  594. &lt;header&gt;&#13;
  595. &lt;!-- 1b) Header --&gt;&#13;
  596. &lt;/header&gt;&#13;
  597. &lt;div class="body"&gt;&#13;
  598. &lt;main&gt;&#13;
  599. &lt;!-- 2) Search Results --&gt;&#13;
  600. &lt;span id="asyncFragment0Placeholder"&gt;&lt;/span&gt;&#13;
  601. &lt;/main&gt;&#13;
  602. &lt;section class="filters"&gt;&#13;
  603. &lt;!-- 3) Search filters --&gt;&#13;
  604. &lt;span id="asyncFragment1Placeholder"&gt;&lt;/span&gt;&#13;
  605. &lt;/section&gt;&#13;
  606. &lt;section class="ads"&gt;&#13;
  607. &lt;!-- 4) Ads --&gt;&#13;
  608. &lt;span id="asyncFragment2Placeholder"&gt;&lt;/span&gt;&#13;
  609. &lt;/section&gt;&#13;
  610. &lt;/div&gt;&#13;
  611. &lt;footer&gt;&#13;
  612. &lt;!-- 5a) Footer --&gt;&#13;
  613. &lt;/footer&gt;&#13;
  614. &#13;
  615. &lt;!-- 5b) Body &lt;script&gt; tags --&gt;&#13;
  616. &#13;
  617. &lt;script&gt;&#13;
  618. window.$af=function(){&#13;
  619. // Small amount of code to support rearranging DOM nodes&#13;
  620. // Unminified:&#13;
  621. // https://github.com/raptorjs/marko-async/blob/master/client-reorder-runtime.js&#13;
  622. };&#13;
  623. &lt;/script&gt;&#13;
  624. &#13;
  625. &lt;div id="asyncFragment1" style="display:none"&gt;&#13;
  626. &lt;!-- 4) Ads content --&gt;&#13;
  627. &lt;/div&gt;&#13;
  628. &lt;script&gt;$af(1)&lt;/script&gt;&#13;
  629. &#13;
  630. &lt;div id="asyncFragment2" style="display:none"&gt;&#13;
  631. &lt;!-- 3) Search filters content --&gt;&#13;
  632. &lt;/div&gt;&#13;
  633. &lt;script&gt;$af(2)&lt;/script&gt;&#13;
  634. &#13;
  635. &lt;div id="asyncFragment0" style="display:none"&gt;&#13;
  636. &lt;!-- 2) Search results content --&gt;&#13;
  637. &lt;/div&gt;&#13;
  638. &lt;script&gt;$af(0)&lt;/script&gt;&#13;
  639. &#13;
  640. &lt;/body&gt;&#13;
  641. &lt;/html&gt;&#13;
  642. </pre>
  643. <p>One caveat with out-of-order flushing is that it requires JavaScript running on the client to move each out-of-order fragment into its proper place in the DOM. Thus, you would only want to enable out-of-order flushing if you know that the client’s web browser has JavaScript enabled. Also, moving DOM nodes may cause the page to be reflowed, which can be visually jarring to the user and result in more client-side CPU usage. If reflow is an issue then there are tricks that can be used to avoid a reflow (e.g., reserving space as part of the initial wireframe). Marko also allows alternative content to be shown while waiting for an out-of-order async fragment.</p>
  644. <p>To enable out-of-order flushing with Marko, the <code>client-reorder="true"</code> attribute must be added to each <code>&lt;async-fragment&gt;</code> tag, and the <code>&lt;async-fragments&gt;</code> tag must be added to the end of the page to serve as the container for rendered out-of-order fragments. Here is the updated <code>&lt;async-fragment&gt;</code> tag for the search results fragment:</p>
  645. <pre>&lt;async-fragment data-provider="data.searchResultsProvider"&#13;
  646. var="searchResults"&#13;
  647. client-reorder="true&gt;&#13;
  648. ...&#13;
  649. &lt;/async-fragment&gt;</pre>
  650. <p>The updated HTML page template with the new <code>&lt;async-fragments&gt;</code> tag is shown below:</p>
  651. <pre>&lt;html&gt;&#13;
  652. &lt;head&gt;&#13;
  653. &lt;title&gt;Clothing Store&lt;/title&gt;&#13;
  654. &lt;!-- Head &lt;link&gt; tags &gt;&#13;
  655. &lt;/head&gt;&#13;
  656. &lt;body&gt;&#13;
  657. ...&#13;
  658. &#13;
  659. &lt;!-- Body &lt;script&gt; tags --&gt;&#13;
  660. &#13;
  661. &lt;async-fragments/&gt;&#13;
  662. &lt;/body&gt;&#13;
  663. &lt;/html&gt;</pre>
  664. <p>In combination with out-of-order flushing, it may be beneficial to move <code>&lt;script&gt;</code> tags that link to external resources to the end of the first chunk (before all of the out-of-order chunks). While the server is busy preparing the rest of the page, the client can start downloading the external JavaScript required to make the page functional. As a result, the user will be able to start interacting with the page sooner.</p>
  665. <p>Our final waterfall with out-of-order flushing will now be similar to the following:</p>
  666. <p><img src="https://docs.google.com/uc?authuser=0&amp;id=0B68ucl6PKE9yanJFNTN3SWxwX2s&amp;export=download" alt="Out-of-order Flush Waterfall Chart"/></p>
  667. <p>The final waterfall shows that the strategy of out-of-order flushing of asynchronous fragments can significantly improve the load time <em>and</em> perceived load time of a page. The user will be met with a progressive loading of a page that is ready to be interacted with sooner.</p>
  668. <h2>Additional considerations</h2>
  669. <h3>HTTP Transport and HTML compression</h3>
  670. <p>To allow HTML to be served in parts, <a href="http://en.wikipedia.org/wiki/Chunked_transfer_encoding">chunked transfer encoding</a> should be used for the HTTP response. Chunked transfer encoding uses delimiters to break up the response, and each flush results in a new chunk. If gzip compression is enabled (and it should be) then flushing the pending data to the gzip stream will result in a gzip data frame being written to the response as part of each chunk. Flushing too often will negatively impact the effectiveness of the compression algorithm, but without flushing periodically then progressive HTML rendering will not be available. By default, Marko will flush at the beginning of an <code>&lt;async-fragment&gt;</code> block (in order to send everything that has already completed), as well as when an async fragment completes. This default strategy results in efficient progressive loading of an HTML page as long as there are not too many async fragments.</p>
  671. <h3>Binding behavior</h3>
  672. <p>For improved usability and responsiveness, there should not be a long delay between rendered HTML being displayed to the user in the web browser and behavior being attached to the associated DOM. At eBay, we use the <a href="https://github.com/raptorjs/marko-widgets">marko-widgets</a> module to bind behavior to DOM nodes. Marko Widgets supports binding behavior to rendered widgets immediately after each independent async fragment, as illustrated in the <a href="https://marko-progressive-rendering.herokuapp.com/">accompanying sample app</a>. For immediate binding to work, the required JavaScript code must be included <em>earlier</em> in the page. For more details, please see the marko-widgets module documentation.</p>
  673. <h3>Error handling</h3>
  674. <p>It is important to note that as soon as a byte is flushed for the HTTP body, then the response is committed; no additional HTTP headers can be sent (e.g., no server-side redirects or cookie-setting), and the HTML that has been sent cannot be “unsent”. Therefore, if an asynchronous data provider errors or times out, then the app must be prepared to show alternative content for <em>that</em> particular async fragment. Please see the documentation for the <a href="https://github.com/raptorjs/marko-async">marko-async</a> module for additional information on how to show alternative content in case of an error.</p>
  675. <h2>Summary</h2>
  676. <p>The Async Fragments technique allows web developers to maximize the benefits of progressive HTML rendering to produce web pages that have improved actual and perceived load times. Developers at eBay have found the concept of binding HTML template fragments to asynchronous data providers easy to grasp and utilize. In addition, the flexibility to support both in-order and out-of-order flushing of async fragments makes this technique applicable for all web browsers and user agents.</p>
  677. <p>The <a href="https://github.com/raptorjs/marko">Marko</a> templating engine is being used as part of eBay’s latest Node.js stack to improve performance while also simplifying how pages are constructed on both the server and the client. Marko is one of a few templating engines for Node.js and web browsers that support streaming, flushing, and asynchronous rendering. Marko has a simple HTML-based syntax, and the Marko compiler produces small and efficient JavaScript modules as output. We encourage you to <a href="http://raptorjs.org/marko/try-online/">try Marko online</a> and in your next Node.js project. Because Marko is a key component of eBay’s internal Node.js stack, and given that it is heavily documented and tested, you can be confident that it will be well supported.</p>
  678. <p><em>Patrick Steele-Idem is a member of eBay’s platform team who enjoys writing open-source software and improving how web applications are built. He is the author of <a href="http://raptorjs.org/">RaptorJS</a>, a suite of open-source front-end power tools that are being used within and outside eBay. You can follow Patrick on Twitter at <a href="https://twitter.com/psteeleidem">@psteeleidem</a>.</em></p>
  679. </article>
  680. </section>
  681. <nav id="jumpto">
  682. <p>
  683. <a href="/david/blog/">Accueil du blog</a> |
  684. <a href="http://www.ebaytechblog.com/2014/12/08/async-fragments-rediscovering-progressive-html-rendering-with-marko/">Source originale</a> |
  685. <a href="/david/stream/2019/">Accueil du flux</a>
  686. </p>
  687. </nav>
  688. <footer>
  689. <div>
  690. <img src="/static/david/david-larlet-avatar.jpg" loading="lazy" class="avatar" width="200" height="200">
  691. <p>
  692. Bonjour/Hi!
  693. Je suis <a href="/david/" title="Profil public">David&nbsp;Larlet</a>, je vis actuellement à Montréal et j’alimente cet espace depuis 15 ans. <br>
  694. Si tu as apprécié cette lecture, n’hésite pas à poursuivre ton exploration. Par exemple via les <a href="/david/blog/" title="Expériences bienveillantes">réflexions bimestrielles</a>, la <a href="/david/stream/2019/" title="Pensées (dés)articulées">veille hebdomadaire</a> ou en t’abonnant au <a href="/david/log/" title="S’abonner aux publications via RSS">flux RSS</a> (<a href="/david/blog/2019/flux-rss/" title="Tiens c’est quoi un flux RSS ?">so 2005</a>).
  695. </p>
  696. <p>
  697. Je m’intéresse à la place que je peux avoir dans ce monde. En tant qu’humain, en tant que membre d’une famille et en tant qu’associé d’une coopérative. De temps en temps, je fais aussi des <a href="https://github.com/davidbgk" title="Principalement sur Github mais aussi ailleurs">trucs techniques</a>. Et encore plus rarement, <a href="/david/talks/" title="En ce moment je laisse plutôt la place aux autres">j’en parle</a>.
  698. </p>
  699. <p>
  700. Voici quelques articles choisis :
  701. <a href="/david/blog/2019/faire-equipe/" title="Accéder à l’article complet">Faire équipe</a>,
  702. <a href="/david/blog/2018/bivouac-automnal/" title="Accéder à l’article complet">Bivouac automnal</a>,
  703. <a href="/david/blog/2018/commodite-effondrement/" title="Accéder à l’article complet">Commodité et effondrement</a>,
  704. <a href="/david/blog/2017/donnees-communs/" title="Accéder à l’article complet">Des données aux communs</a>,
  705. <a href="/david/blog/2016/accompagner-enfant/" title="Accéder à l’article complet">Accompagner un enfant</a>,
  706. <a href="/david/blog/2016/senior-developer/" title="Accéder à l’article complet">Senior developer</a>,
  707. <a href="/david/blog/2016/illusion-sociale/" title="Accéder à l’article complet">L’illusion sociale</a>,
  708. <a href="/david/blog/2016/instantane-scopyleft/" title="Accéder à l’article complet">Instantané Scopyleft</a>,
  709. <a href="/david/blog/2016/enseigner-web/" title="Accéder à l’article complet">Enseigner le Web</a>,
  710. <a href="/david/blog/2016/simplicite-defaut/" title="Accéder à l’article complet">Simplicité par défaut</a>,
  711. <a href="/david/blog/2016/minimalisme-esthetique/" title="Accéder à l’article complet">Minimalisme et esthétique</a>,
  712. <a href="/david/blog/2014/un-web-omni-present/" title="Accéder à l’article complet">Un web omni-présent</a>,
  713. <a href="/david/blog/2014/manifeste-developpeur/" title="Accéder à l’article complet">Manifeste de développeur</a>,
  714. <a href="/david/blog/2013/confort-convivialite/" title="Accéder à l’article complet">Confort et convivialité</a>,
  715. <a href="/david/blog/2013/testament-numerique/" title="Accéder à l’article complet">Testament numérique</a>,
  716. et <a href="/david/blog/" title="Accéder aux archives">bien d’autres…</a>
  717. </p>
  718. <p>
  719. On peut <a href="mailto:david%40larlet.fr" title="Envoyer un courriel">échanger par courriel</a>. Si éventuellement tu souhaites que l’on travaille ensemble, tu devrais commencer par consulter le <a href="http://larlet.com">profil dédié à mon activité professionnelle</a> et/ou contacter directement <a href="http://scopyleft.fr/">scopyleft</a>, la <abbr title="Société coopérative et participative">SCOP</abbr> dont je fais partie depuis six ans. Je recommande au préalable de lire <a href="/david/blog/2018/cout-site/" title="Attention ce qui va suivre peut vous choquer">combien coûte un site</a> et pourquoi je suis plutôt favorable à une <a href="/david/pro/devis/" title="Discutons-en !">non-demande de devis</a>.
  720. </p>
  721. <p>
  722. Je ne traque pas ta navigation mais mon
  723. <abbr title="Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33.184162340">hébergeur</abbr>
  724. conserve des logs d’accès.
  725. </p>
  726. </div>
  727. </footer>
  728. <script type="text/javascript">
  729. ;(_ => {
  730. const jumper = document.getElementById('jumper')
  731. jumper.addEventListener('click', e => {
  732. e.preventDefault()
  733. const anchor = e.target.getAttribute('href')
  734. const targetEl = document.getElementById(anchor.substring(1))
  735. targetEl.scrollIntoView({behavior: 'smooth'})
  736. })
  737. })()
  738. </script>