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