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. How fast "dev.to" is?
    In-house Tech Meetup vol.1 - 2018/10/23
    @tanakaworld

    View Slide

  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

    View Slide

  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

    View Slide

  4. I love Ruby, but ...

    View Slide

  5. View Slide

  6. Today’s topic is related to Ruby !

    View Slide

  7. View Slide

  8. https://dev.to/

    View Slide

  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”

    View Slide

  10. “Insanely Fast” = めちゃくちゃはやい
    https://dev.to/mizchi/-devto--b5

    View Slide

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

    View Slide

  12. Basic RoR Application
    ● Ruby on Rails
    ● VanillaJS
    ● Postgres
    ● Heroku
    https://github.com/thepracticaldev/dev.to

    View Slide

  13. How fast “dev.to” is?
    +
    Source Codes

    View Slide

  14. Table of Contents
    ● CDN
    ● Service Worker
    ● Optimization of page rendering
    ● Preload
    ● Other Tech stack

    View Slide

  15. CDN

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  21. Purging
    ● e.g.
    ○ On a article updated.
    ● Purge API will be called
    ● ※ #purge, #purge_all won’t be called

    View Slide

  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

    View Slide

  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

    View Slide

  24. Service Worker

    View Slide

  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

    View Slide

  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);
    });
    }

    View Slide

  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() {})

    View Slide

  28. Works on mainly browsers
    https://caniuse.com/#feat=serviceworkers

    View Slide

  29. [Application] Tab
    ● Service Workers
    ● Actions
    ○ Stop
    ○ Update
    ○ Unregister
    ○ Push

    View Slide

  30. [Application] Tab
    ● Cache Storage

    View Slide

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

    View Slide

  32. [Sources] tab
    ● Debug serviceworker.js

    View Slide

  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");
    });
    })
    );
    }

    View Slide

  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)
    }

    View Slide

  35. Offline
    ● dev.to also works offline
    ● offline.html returned from cache
    ● You can enjoy painting

    View Slide

  36. Optimization of
    page rendering

    View Slide

  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

    View Slide

  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 )





    View Slide

  39. Action 1 : No external CSS requests
    ● Render tag in <head> tag<br/>● A partial for inline styles<br/>● CSS will be cached to CDN with Static HTML<br/><%= render "layouts/styles" %><br/><link rel="stylesheet" type="text/css" href="mystyle.css"><br/>

    View Slide

  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 %>
    <br/><% Rails.application.config.assets.compile = true %><br/><%= Rails.application.assets['scaffolds.css'].to_s.html_safe %><br/><%= Rails.application.assets['top-bar.css'].to_s.html_safe %><br/><% if @article_show %><br/><%= Rails.application.assets['article-show.css'].to_s.html_safe %><br/><%= Rails.application.assets['comments.css'].to_s.html_safe %><br/><%= Rails.application.assets['more-articles.css'].to_s.html_safe %><br/><%= Rails.application.assets["syntax.css"].to_s.html_safe %><br/><%= Rails.application.assets["ltags/LiquidTags.scss"].to_s.html_safe %><br/><%= Rails.application.assets["sticky-nav.css"].to_s.html_safe %><br/><% else %><br/># •••<br/>

    View Slide

  41. Action 2 : Lazy loading CSS
    ● Append CSS after rendering is completed
    ● Inline CSS is needed for first page rendering

    View Slide

  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);
    }
    }
    # •••

    View Slide

  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? %>
    href='https://fonts.googleapis.com/css?family=Roboto' rel='stylesheet'
    type='text/css'
    >
    <%= javascript_include_tag 'application' %>
    <% end %>
    <% end %>

    View Slide

  44. ● Asynchronously load in tag<br/>● No Ads<br/>● No social widgets<br/>● Exc.<br/>○ https://dev.to/membership<br/>○ Stripe<br/>Action 4 : No synchronous JavaScript<br/>Image from https://html.spec.whatwg.org/multipage/scripting.html#attr-script-async<br/>

    View Slide

  45. Preload

    View Slide

  46. DEMO

    View Slide

  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 tag
    ○ Change URL by pushState

    View Slide

  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",""))
    }

    View Slide

  49. Other Tech stack

    View Slide

  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

    View Slide

  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/

    View Slide

  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.

    View Slide

  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

    View Slide

  54. Summary

    View Slide

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


    View Slide

  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

    View Slide

  57. Thanks

    View Slide

  58. References

    View Slide

  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

    View Slide

  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/

    View Slide

  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

    View Slide