How fast "dev.to" is?

851704b2aa97d2117dc89bf60a2cd272?s=47 tanakaworld
October 23, 2018

How fast "dev.to" is?

851704b2aa97d2117dc89bf60a2cd272?s=128

tanakaworld

October 23, 2018
Tweet

Transcript

  1. How fast "dev.to" is? In-house Tech Meetup vol.1 - 2018/10/23

    @tanakaworld
  2. About Me • Yutaro Tanaka ◦ Slack: @tanakaworld ◦ Twitter:

    @_tanakaworld ◦ GitHub: tanakaworld • Merpay ◦ Software Engineer (Frontend) ◦ Vue.js / Nuxt / TypeScript • Love ◦ JavaScript / Node.js ◦ Ruby / Rails
  3. Developing proff.io with my friend • CV/Resume management web service

    • Proff = プロフ • Tech Stack ◦ Ruby / Rails ◦ Vue.js ◦ AWS My Product Releases https://jp.techcrunch.com/2018/08/20/proff-launched/ https://prtimes.jp/main/html/rd/p/000000003.000036442.html https://proff.io
  4. I love Ruby, but ...

  5. None
  6. Today’s topic is related to Ruby !

  7. None
  8. https://dev.to/

  9. What is “dev.to” • https://dev.to • An online community service

    for developers • Sharing and discovering tech ideas • Created in 2016 (NY) • Became a hot topic in 2017 (Japan) • “Insanely Fast”
  10. “Insanely Fast” = めちゃくちゃはやい https://dev.to/mizchi/-devto--b5

  11. OSS https://dev.to/ben/devto-is-now-open-source-5n1

  12. Basic RoR Application • Ruby on Rails • VanillaJS •

    Postgres • Heroku https://github.com/thepracticaldev/dev.to
  13. How fast “dev.to” is? + Source Codes

  14. Table of Contents • CDN • Service Worker • Optimization

    of page rendering • Preload • Other Tech stack
  15. CDN

  16. • Generate dynamic web page in Server Side • Dynamic

    web page ◦ Data from DB ◦ Data from Network ↓ ◦ Embed to View Template ↓ ◦ Response completed HTML to the browser • Therefore ◦ We cannot store it to CDN General Rails Application
  17. dev.to’s Policy • Separate “static” and “dynamic” contents • Static

    ◦ Serve HTML page from the closest CDN ◦ Pre-gzipped • Dynamic ◦ Based on user session ▪ Likes count ▪ Comments ▪ Notifications
  18. Fastly • Edge Cache ◦ For reducing network latency ◦

    e.g. visit this site from New York, you will be served a static HTML page right from New York. • Control Fastly caches by “Surrogate-Control” header • Control browser caches by “Cache-Control” header • fastly-rails gem https://docs.fastly.com/guides/basic-concepts/how-caching-and-cdns-work
  19. app/controllers/stories_controller.rb class StoriesController < ApplicationController # [No cache in browser]

    # Cache-Control: 'public, no-cache' before_action :set_cache_control_headers, only: %i[index search show] def index # [Cache key] 'articles articles/1, articles/2, ...' set_surrogate_key_header "articles", @stories.map(&:record_key) # ############################################################################## # [Surrogate-Control] # 600 sec = 10 min response.headers["Surrogate-Control"] = "max-age=600, stale-while-revalidate=30, stale-if-error=86400" render template: "articles/index" end end
  20. app/controllers/applications_controller.rb def set_no_cache_header response.headers["Cache-Control"] = "no-cache, no-store" response.headers["Pragma"] = "no-cache"

    response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT" end
  21. Purging • e.g. ◦ On a article updated. • Purge

    API will be called • ※ #purge, #purge_all won’t be called
  22. app/labor/cache_buster.rb class CacheBuster # ••• def bust(path) return unless Rails.env.production?

    HTTParty.post("https://api.fastly.com/purge/https://dev.to#{path}", headers: { "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] }) HTTParty.post("https://api.fastly.com/purge/https://dev.to#{path}?i=i", headers: { "Fastly-Key" => ApplicationConfig["FASTLY_API_KEY"] }) end def bust_article(article) bust("/" + article.user.username) bust(article.path + "/") bust(article.path + "?i=i") bust(article.path + "/?i=i") bust(article.path + "/comments") bust(article.path + "?preview=" + article.password) bust(article.path + "?preview=" + article.password + "&i=i") # ••• end
  23. app/models/article.rb class Article < ApplicationRecord after_save :bust_cache # ••• def

    bust_cache if Rails.env.production? cache_buster = CacheBuster.new cache_buster.bust(path) cache_buster.bust(path + "?i=i") cache_buster.bust(path + "?preview=" + password) async_bust end end def async_bust CacheBuster.new.bust_article(self) HTTParty.get GeneratedImage.new(self).social_image if published end
  24. Service Worker

  25. What is Service Worker ? • A Event-Driven worker •

    Runs in the background, has separated thread from a web page thread • Only run over HTTPS (or localhost) • Essentially act as proxy servers
  26. Life Cycle Image from https://developers.google.com/web/fundamentals/primers/service-workers/ if ('serviceWorker' in navigator) {

    navigator.serviceWorker .register('/serviceworker.js', { scope: '/' }) .then(function swStart(registration) { // registered! }) .catch(function (error) { console.log('ServiceWorker registration failed: ', error); }); }
  27. app/assets/javascripts/serviceworker.js.erb self.addEventListener('install', function onInstall() {}); self.addEventListener('activate', function onActivate() {}); self.addEventListener('fetch',

    function onFetch() {}); self.addEventListener('push', function onPush() {}); self.addEventListener('notificationclick', function onNotificationClick() {})
  28. Works on mainly browsers https://caniuse.com/#feat=serviceworkers

  29. [Application] Tab • Service Workers • Actions ◦ Stop ◦

    Update ◦ Unregister ◦ Push
  30. [Application] Tab • Cache Storage

  31. [Network] tab • “⚙” means a request from Service Worker

  32. [Sources] tab • Debug serviceworker.js

  33. Event: onInstall • Download contents depends on deploy version function

    onInstall(event) { event.waitUntil( caches.open(CACHE_NAME).then(function prefill(cache) { return cache.addAll([ '<%= asset_path "base.js" %>', '<%= asset_path "minimal.css" %>', '/offline.html', '<%= asset_path "devword.png" %>', '<%= asset_path "wires.png" %>', '<%= asset_path "comments-bubble.png" %>', '<%= asset_path "reactions-stack.png" %>', '<%= asset_path "readinglist-button.png" %>', '<%= asset_path "emoji/emoji-one-heart.png" %>', '<%= asset_path "emoji/emoji-one-unicorn.png" %>', '<%= asset_path "emoji/emoji-one-bookmark.png" %>', '<%= asset_path "emoji/apple-fire.png" %>', ]).then(function () { console.log("WORKER: Install completed"); }); }) ); }
  34. Event: onFetch • Check whether cacheable content • If cacheable

    cached content exists, get it from the cache event.respondWith( caches.match(event.request).then(function (cached) { if (cached && shouldReturnStraightFromCache(event.request.url)) { return cached; } return fetch(event.request) .then(fetchedFromNetwork) .catch(function fallback() { return cached || caches.match('/offline.html'); }) } ) function shouldReturnStraightFromCache(url) { return (url === "<%= asset_path "base.js" %>" || url === "<%= asset_path "minimal.css" %>" || url.indexOf(".self-") > -1 || (url.indexOf("search?") > -1 && url.indexOf("&i=i") > -1) || url.indexOf("readinglist?i=i") > -1 || url.indexOf("freetls.fastly.net") > -1) }
  35. Offline • dev.to also works offline • offline.html returned from

    cache • You can enjoy painting
  36. Optimization of page rendering

  37. Eliminating render-blockings • Once a page is delivered, the next

    important is rendering it as quickly as possible • Eliminate all rendering-blocking latency In dev.to • No external CSS requests • No custom fonts • No synchronous JavaScript
  38. Rendering-block CSS • CSS is a render blocking resource •

    The browser blocks rendering until it has both the DOM and the CSSOM • Need to deliver CSS as quickly as possible • ( Also avoid rendering-block CSS by media query ) <link href="style.css" rel="stylesheet"> <link href="style.css" rel="stylesheet" media="all"> <link href="print.css" rel="stylesheet" media="print"> <link href="other.css" rel="stylesheet" media="(min-width: 40em)"> <link href="portrait.css" rel="stylesheet" media="orientation:portrait">
  39. Action 1 : No external CSS requests • Render <style>

    tag in <head> tag • A partial for inline styles • CSS will be cached to CDN with Static HTML <%= render "layouts/styles" %> <link rel="stylesheet" type="text/css" href="mystyle.css">
  40. app/views/layouts/_styles.html.erb <% cache "base_inline_styles_#{@story_show.to_s}_ ••• _#{@tags_index}_#{ApplicationConfig["DEPLOYMENT_SIGNATURE"].to_s}x_xs__", :expires_in => 6.hours do

    %> <% if @story_show %> <style> <% Rails.application.config.assets.compile = true %> <%= Rails.application.assets['scaffolds.css'].to_s.html_safe %> <%= Rails.application.assets['top-bar.css'].to_s.html_safe %> <% if @article_show %> <%= Rails.application.assets['article-show.css'].to_s.html_safe %> <%= Rails.application.assets['comments.css'].to_s.html_safe %> <%= Rails.application.assets['more-articles.css'].to_s.html_safe %> <%= Rails.application.assets["syntax.css"].to_s.html_safe %> <%= Rails.application.assets["ltags/LiquidTags.scss"].to_s.html_safe %> <%= Rails.application.assets["sticky-nav.css"].to_s.html_safe %> <% else %> # •••
  41. Action 2 : Lazy loading CSS • Append CSS after

    rendering is completed • Inline CSS is needed for first page rendering
  42. app/assets/javascripts/initializers/initializeStylesheetAppend.js.erb function initializeStylesheetAppend() { if (!document.getElementById("main-head-stylesheet")){ var link = document.createElement('link');

    link.type = 'text/css' link.id = "main-head-stylesheet" link.rel = 'stylesheet' link.href = '<%= stylesheet_url 'minimal' %>'; document.getElementsByTagName('head')[0].appendChild(link); } } # •••
  43. Action 3 : No Custom Fonts • So heavy …

    • Dev.to is partially loads custom font <% if !core_pages? %> <%= stylesheet_link_tag 'application', media: 'all' %> <% if render_js? %> <link href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css' > <%= javascript_include_tag 'application' %> <% end %> <% end %>
  44. • Asynchronously load in <script> tag • No Ads •

    No social widgets • Exc. ◦ https://dev.to/membership ◦ Stripe Action 4 : No synchronous JavaScript Image from https://html.spec.whatwg.org/multipage/scripting.html#attr-script-async
  45. Preload

  46. DEMO

  47. Preload the page • Customize instantClick ◦ http://instantclick.io/ • Fake

    page transition ◦ pjax = pushState + Ajax • On hover a link ◦ Asynchronous get page by XHRHttpRequest ◦ Store page body to JavaScript object • On Click a link ◦ Replace children of <body> tag ◦ Change URL by pushState
  48. app/assets/javascripts/base.js.erb $fetchedBodies = {} // OnHover =========================================================== $fetchedBodies[url] = {body:body,

    title:title}; // OnClick ============================================================ if($fetchedBodies[url]){ var body = $fetchedBodies[url]['body']; var title = $fetchedBodies[url]['title']; document.getElementsByTagName("BODY")[0].replaceChild( body, document.getElementById("page-content") ) history.pushState(null, null, url.replace("?samepage=true","").replace("&samepage=true","")) }
  49. Other Tech stack

  50. Cloudinary • https://cloudinary.com/ • Images are automatically optimized for compression

    • Served from the most efficient format depending on the browser ◦ webp for Chrome ◦ jpeg for Safari ◦ etc... • Cloudinary fully leverages HTTP2 • In dev.to ◦ cloudinary_gem • Similar service to ImageFlux in Mercari ◦ https://case.sakura.ad.jp/case/mercari-imageflux ◦ https://tech.mercari.com/entry/2018/01/30/161001
  51. Algolia • https://www.algolia.com/ • Search engine for website contents •

    Just upload JSON file • In dev.to ◦ algoliasearch-rails • Algolia is well used in OSS documents ◦ e.g . https://vuejs.org/
  52. Stream • https://getstream.io/ • Building news feeds and activity streams

    • In dev.to ◦ stream-rails ◦ Bind Rails model to feeds in ActiveRecord ◦ comment, follow, mention, notification, reaction etc.
  53. And more • Gemfile ◦ https://github.com/thepracticaldev/dev.to/blob/master/Gemfile • The dev.to tech

    stack ◦ https://dev.to/ben/the-devto-tech-stack
  54. Summary

  55. Performance on the web is the most important UX consideration

    ” “
  56. How fast “dev.to” is ? • “dev.to” is ◦ a

    general RoR application ◦ saied “Slow”, but you can make it “Insanely Fast” ◦ good OSS for studying about Ruby on Rails • All For One ◦ Performance on the web • Focus to ◦ CDN + Service Worker > Reducing network latency ◦ Preload / Lazy Load / No Load > Reducing rendering latency
  57. Thanks

  58. References

  59. Fastly • How caching and CDNs work ◦ https://docs.fastly.com/guides/basic-concepts/how-caching-and-cdns-work •

    Cache control tutorial ◦ https://docs.fastly.com/guides/tutorials/cache-control-tutorial • Serving stale content ◦ https://docs.fastly.com/guides/performance-tuning/serving-stale-content
  60. Service Worker • Service Worker API ◦ https://developer.mozilla.org/docs/Web/API/ServiceWorker_API • Service

    Workers: an Introduction ◦ https://developers.google.com/web/fundamentals/primers/service-workers/ • The Service Worker Lifecycle ◦ https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle • Debugging Service Workers ◦ https://developers.google.com/web/fundamentals/codelabs/debugging-service-workers/
  61. Articles of Ben Halpern (founder) • Making dev.to Incredibly fast

    ◦ https://dev.to/ben/making-devto-insanely-fast • How I Made this Website Hella Fast Without Overcomplicating Things ◦ https://dev.to/ben/how-i-made-this-website-hella-fast-without-overcomplicating-things • What it Takes to Render a Complex Web App in Milliseconds ◦ https://dev.to/ben/what-it-takes-to-render-a-complex-webapp-in-milliseconds • What the heck is a "Progressive Web App"? Seriously. ◦ https://dev.to/ben/what-the-heck-is-a-progressive-web-app-seriously-923