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

How fast "dev.to" is?

tanakaworld
October 23, 2018

How fast "dev.to" is?

tanakaworld

October 23, 2018
Tweet

More Decks by tanakaworld

Other Decks in Technology

Transcript

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

    @_tanakaworld ◦ GitHub: tanakaworld • Merpay ◦ Software Engineer (Frontend) ◦ Vue.js / Nuxt / TypeScript • Love ◦ JavaScript / Node.js ◦ Ruby / Rails
  2. 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
  3. 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”
  4. Basic RoR Application • Ruby on Rails • VanillaJS •

    Postgres • Heroku https://github.com/thepracticaldev/dev.to
  5. Table of Contents • CDN • Service Worker • Optimization

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

  7. • 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
  8. 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
  9. 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
  10. 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
  11. Purging • e.g. ◦ On a article updated. • Purge

    API will be called • ※ #purge, #purge_all won’t be called
  12. 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
  13. 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
  14. 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
  15. 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); }); }
  16. 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() {})
  17. 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"); }); }) ); }
  18. 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) }
  19. 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
  20. 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">
  21. 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">
  22. 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 %> # •••
  23. Action 2 : Lazy loading CSS • Append CSS after

    rendering is completed • Inline CSS is needed for first page rendering
  24. 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); } } # •••
  25. 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 %>
  26. • 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
  27. 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
  28. 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","")) }
  29. 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
  30. 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/
  31. 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.
  32. 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
  33. 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
  34. 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/
  35. 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