Skip to content
This repository has been archived by the owner on Apr 30, 2019. It is now read-only.

Commit

Permalink
Performance improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
akyoto committed Nov 16, 2016
1 parent 9d1c6ff commit 94b6ec4
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 52 deletions.
5 changes: 5 additions & 0 deletions lib/App/etag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const crypto = require('crypto')

module.exports = function(data) {
return crypto.createHash('sha1').update(data).digest('hex')
}
3 changes: 1 addition & 2 deletions lib/App/onFileRequest.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const fs = require('fs')
const xxhash = require('xxhash')
const path = require('path')
const getMIMEType = require('mime-types').lookup

Expand Down Expand Up @@ -52,7 +51,7 @@ module.exports = function(request, response) {

const headers = {
'Content-Length': stats.size,
'ETag': xxhash.hash(Buffer.from(stats.mtime.toString()), 0),
'ETag': this.etag(stats.mtime.toString()),
'Cache-Control': 'max-age=864000'
}

Expand Down
80 changes: 55 additions & 25 deletions lib/App/routePage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const xxhash = require('xxhash')
const zlib = require('zlib')
const crypto = require('crypto')
const NodeCache = require('node-cache')

// This should be close to the MTU size of a TCP packet.
// Regarding performance it makes no sense to compress smaller files.
Expand All @@ -9,6 +10,15 @@ const zlib = require('zlib')
// we're trying to optimize for performance, not bandwidth.
const gzipThreshold = 1450

// We cache the gzipped response using the ETag as a key
// for the in-memory cache. By default we save the cached
// responses only for 10 minutes to prevent using a lot of memory.
const gzipCache = new NodeCache({
stdTTL: 600,
checkperiod: 610
})

// GZip compression options
const bestCompressionOptions = {
level: zlib.Z_BEST_COMPRESSION
}
Expand All @@ -17,14 +27,19 @@ const fastCompressionOptions = {
level: zlib.Z_BEST_SPEED
}

// Boilerplate HTML content
const mobileMetaTag = '<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=yes">'
const manifestTag = '<link rel="manifest" href="/manifest.json">'
const scriptTag = '<script src="/scripts.js"></script>'

// Hash a UTF-8 string using SHA-1
function hash(code) {
return crypto.createHash('sha1').update(code, 'utf8').digest('hex')
}

// Generic respond functionality
const respond = function(code, headers, request, response) {
let codeBuffer = Buffer.from(code)
let etag = xxhash.hash(codeBuffer, 0).toString()
function respond(code, headers, request, response) {
let etag = hash(code)

if(request.headers['if-none-match'] === etag) {
response.writeHead(304)
Expand All @@ -42,24 +57,36 @@ const respond = function(code, headers, request, response) {
localHeaders.ETag = etag

// Send response
if(!request.fromProxy && code.length >= gzipThreshold) {
if(code.length >= gzipThreshold) {
localHeaders['Content-Encoding'] = 'gzip'

let gzippedCode = gzipCache.get(etag)

if(gzippedCode !== undefined) {
localHeaders['Content-Length'] = gzippedCode.length

response.writeHead(response.statusCode || 200, localHeaders)
response.end(gzippedCode)
return
}

zlib.gzip(code, fastCompressionOptions, function(error, gzippedCode) {
localHeaders['Content-Length'] = gzippedCode.length

response.writeHead(response.statusCode || 200, localHeaders)
response.end(gzippedCode)

gzipCache.set(etag, gzippedCode)
})
} else {
localHeaders['Content-Length'] = Buffer.byteLength(code, 'utf8')
localHeaders['Content-Length'] = Buffer.byteLength(code, 'utf8') //codeBuffer.length

response.writeHead(response.statusCode || 200, localHeaders)
response.end(code)
}
}

const respondStatic = function(code, baseHeaders) {
function respondStatic(code, baseHeaders) {
const headers = Object.assign({}, baseHeaders)

headers['Cache-Control'] = 'no-cache'
Expand All @@ -72,7 +99,7 @@ const respondStatic = function(code, baseHeaders) {
const gzippedCode = zlib.gzipSync(code, bestCompressionOptions)

headers['Content-Length'] = gzippedCode.length
headers.ETag = xxhash.hash(Buffer.from(gzippedCode), 0).toString()
headers.ETag = hash(gzippedCode)

return function(request, response) {
if(request.headers['if-none-match'] === headers.ETag) {
Expand All @@ -89,7 +116,7 @@ const respondStatic = function(code, baseHeaders) {
// Keep in mind that the client needs to uncompress and that takes time as well.
// Therefore we send an uncompressed version.
headers['Content-Length'] = Buffer.byteLength(code, 'utf8')
headers.ETag = xxhash.hash(Buffer.from(code), 0).toString()
headers.ETag = hash(code)

return function(request, response) {
if(request.headers['if-none-match'] === headers.ETag) {
Expand Down Expand Up @@ -188,6 +215,9 @@ module.exports = function(page) {
return
}

// So we can avoid the bind calls
const app = this

// Routing
if(page.controller) {
if(page.template) {
Expand All @@ -203,24 +233,24 @@ module.exports = function(page) {
page.httpVerbs.forEach(method => {
const next = page.controller[method].bind(page.controller)

this[method](page.url, (request, response) => {
renderLayout(request, layoutControllerParams => {
response.render = params => {
const code = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))
this[method](page.url, function requestHandler(request, response) {
renderLayout(request, function handleLayout(layoutControllerParams) {
response.render = function handlePage(params) {
const content = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))

if(layoutControllerParams) {
if(layoutData || request.globals)
Object.assign(layoutControllerParams, request.globals, layoutData)

layoutControllerParams.content = code
layoutControllerParams.app = this
layoutControllerParams.content = content
layoutControllerParams.app = app
layoutControllerParams.page = page

respond(renderLayoutTemplate(layoutControllerParams), headers, request, response)
} else {
respond(renderLayoutTemplate(Object.assign({
content: code,
app: this,
content,
app,
page
},
request.globals,
Expand All @@ -242,13 +272,13 @@ module.exports = function(page) {
page.httpVerbs.forEach(method => {
const runPageController = page.controller[method].bind(page.controller)

this[method](page.url, (request, response) => {
response.render = params => {
const code = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))
this[method](page.url, function requestHandler(request, response) {
response.render = function handlePage(params) {
const content = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))

const layoutParams = {
content: code,
app: this,
content,
app,
page
}

Expand Down Expand Up @@ -280,16 +310,16 @@ module.exports = function(page) {
const renderLayout = this.layout.controller.render.bind(this.layout.controller)
const layoutData = this.layout.json

this.get(page.url, (request, response) => {
renderLayout(request, layoutControllerParams => {
this.get(page.url, function(request, response) {
renderLayout(request, function(layoutControllerParams) {
if(!layoutControllerParams)
layoutControllerParams = {}

if(layoutData || request.globals)
Object.assign(layoutControllerParams, request.globals, layoutData)

layoutControllerParams.content = page.code
layoutControllerParams.app = this
layoutControllerParams.app = app
layoutControllerParams.page = page

respond(renderLayoutTemplate(layoutControllerParams), headers, request, response)
Expand Down
3 changes: 1 addition & 2 deletions lib/App/routeScripts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const compress = require('brotli').compress
const xxhash = require('xxhash')

module.exports = function() {
const combinedJS = '"use strict";' + (this.liveReload ? this.liveReload.script : '') + this.pluginScripts.join(';') + this.js.map(script => script.code).join(';')
Expand Down Expand Up @@ -27,7 +26,7 @@ module.exports = function() {
compressedScriptsHeaders['Content-Length'] = Buffer.byteLength(compressedScripts, 'utf8')
}

compressedScriptsHeaders.ETag = xxhash.hash(Buffer.from(compressedScripts), 0)
compressedScriptsHeaders.ETag = this.etag(compressedScripts)

// Route
this.server.routes.GET['scripts.js'] = (request, response) => {
Expand Down
3 changes: 1 addition & 2 deletions lib/App/routeStyles.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const compress = require('brotli').compress
const xxhash = require('xxhash')

module.exports = function() {
this.combinedCSS = this.pluginStyles.join(' ') + this.css.map(style => style.code).join(' ')
Expand Down Expand Up @@ -27,7 +26,7 @@ module.exports = function() {
compressedStylesHeaders['Content-Length'] = Buffer.byteLength(compressedStyles, 'utf8')
}

compressedStylesHeaders.ETag = xxhash.hash(Buffer.from(compressedStyles), 0)
compressedStylesHeaders.ETag = this.etag(compressedStyles)

// Route
this.server.routes.GET['styles.css'] = (request, response) => {
Expand Down
8 changes: 2 additions & 6 deletions lib/Server/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,15 @@ let run = function(app) {
this.redirectServer = require('http').createServer((request, response) => {
// If there is a proxy sending our HTTP contents to an HTTPS client we allow
// accessing the contents directly via HTTP without redirecting to HTTPS.
if(request.headers['x-forwarded-proto'] === 'https') {
request.fromProxy = true
if(request.headers['x-forwarded-proto'] === 'https')
return this.onRequest(request, response)
}

// In production mode we allow local proxy servers to access HTTP contents.
if(app.production) {
let remoteAddress = request.connection.remoteAddress

if(remoteAddress === '::ffff:127.0.0.1' || remoteAddress === '127.0.0.1') {
request.fromProxy = true
if(remoteAddress === '::ffff:127.0.0.1' || remoteAddress === '127.0.0.1')
return this.onRequest(request, response)
}
}

// If there's no proxy (e.g. direct browser access to HTTP) we redirect
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aero",
"version": "2.0.3",
"version": "2.0.4",
"description": "The fastest node.js framework.",
"repository": "aerojs/aero",
"homepage": "https://github.com/aerojs/aero",
Expand All @@ -17,6 +17,7 @@
"markdown-it": "8.x",
"mime-types": "2.x",
"mkdirp": "0.x",
"node-cache": "4.x",
"node-watch": "0.x",
"pem": "1.x",
"pug": "2.0.0-beta6",
Expand All @@ -27,8 +28,7 @@
"stylus": ">=0.54.0",
"uglify-js-harmony": ">=2.6",
"uws": "0.x",
"ws": ">=1.1.1",
"xxhash": "0.x"
"ws": ">=1.1.1"
},
"devDependencies": {
"aero-ajax": "*",
Expand Down
1 change: 0 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ require('strict-mode')(function () {
assert(app.config.ports)
assert(app.config.ports.http)
assert(app.config.ports.https)
assert(app.config.ports.liveReload)

assert(app.package)
assert(app.package.name)
Expand Down
27 changes: 16 additions & 11 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ character-parser@^2.1.1:
is-regex "^1.0.3"

clean-css@^3.2.10, clean-css@^3.3.0:
version "3.4.20"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.20.tgz#c0d8963b5448e030f0bcd3ddd0dac4dfe3dea501"
version "3.4.21"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.21.tgz#2101d5dbd19d63dbc16a75ebd570e7c33948f65b"
dependencies:
commander "2.8.x"
source-map "0.4.x"
Expand All @@ -232,6 +232,10 @@ cliui@^3.0.3:
strip-ansi "^3.0.1"
wrap-ansi "^2.0.0"

clone@1.0.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"

code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
Expand Down Expand Up @@ -816,6 +820,10 @@ load-class@1.x:
dependencies:
bluebird "*"

lodash@4.x:
version "4.17.2"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42"

log-driver@1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056"
Expand Down Expand Up @@ -886,9 +894,12 @@ multi-stage-sourcemap@0.2.1:
dependencies:
source-map "^0.1.34"

nan@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232"
node-cache@4.x:
version "4.1.0"
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-4.1.0.tgz#2a6a66460bf063781138206988237ec02c135157"
dependencies:
clone "1.0.x"
lodash "4.x"

node-uuid@~1.4.7:
version "1.4.7"
Expand Down Expand Up @@ -1611,12 +1622,6 @@ xtend@^4.0.0, xtend@~4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"

xxhash@0.x:
version "0.2.4"
resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.2.4.tgz#8b8a48162cfccc21b920fa500261187d40216c39"
dependencies:
nan "^2.4.0"

y18n@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
Expand Down

0 comments on commit 94b6ec4

Please sign in to comment.