Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

About Me ● Yutaro Tanaka ○ Slack: @tanakaworld ○ Twitter: @_tanakaworld ○ GitHub: tanakaworld ● Merpay ○ Software Engineer (Frontend) ○ Vue.js / Nuxt / TypeScript ● Love ○ JavaScript / Node.js ○ Ruby / Rails

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

I love Ruby, but ...

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Today’s topic is related to Ruby !

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

https://dev.to/

Slide 9

Slide 9 text

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”

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

CDN

Slide 16

Slide 16 text

● 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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Service Worker

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

[Application] Tab ● Cache Storage

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

[Sources] tab ● Debug serviceworker.js

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Optimization of page rendering

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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 )

Slide 39

Slide 39 text

Action 1 : No external CSS requests ● Render 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">

Slide 40

Slide 40 text

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 %> <% 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 %> # •••

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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? %> <%= javascript_include_tag 'application' %> <% end %> <% end %>

Slide 44

Slide 44 text

● Asynchronously load in 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

Slide 45

Slide 45 text

Preload

Slide 46

Slide 46 text

DEMO

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Other Tech stack

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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/

Slide 52

Slide 52 text

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.

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Summary

Slide 55

Slide 55 text

Performance on the web is the most important UX consideration ” “

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Thanks

Slide 58

Slide 58 text

References

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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/

Slide 61

Slide 61 text

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