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

How fast "dev.to" is?

Avatar for tanakaworld tanakaworld
October 23, 2018

How fast "dev.to" is?

Avatar for tanakaworld

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