Skip to content
This repository has been archived by the owner on Dec 5, 2022. It is now read-only.

(perf): improve parsing of link header from fragments #248

Merged
merged 2 commits into from
Aug 12, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions lib/fragment.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict';

const ContentLengthStream = require('./streams/content-length-stream');
const EventEmitter = require('events').EventEmitter;
const PassThrough = require('stream').PassThrough;
const LinkHeader = require('http-link-header');
const zlib = require('zlib');
const ContentLengthStream = require('./streams/content-length-stream');
const parseLinkHeader = require('./parse-link-header');
const { getFragmentAssetUris } = require('./utils');

const { globalTracer, Tags } = require('opentracing');
const tracer = globalTracer();
Expand Down Expand Up @@ -84,6 +85,8 @@ module.exports = class Fragment extends EventEmitter {
this.requestFragment = requestFragment;
this.pipeInstanceName = pipeInstanceName;
this.stream = new PassThrough();
this.scriptRefs = [];
this.styleRefs = [];
}

/**
Expand Down Expand Up @@ -165,22 +168,21 @@ module.exports = class Fragment extends EventEmitter {
onResponse(response, isFallback, span) {
const { statusCode, headers } = response;

if (!isFallback) {
this.emit('response', statusCode, headers);
}
// Extract the assets from fragment link headers.
const { refs } = LinkHeader.parse(
const refs = parseLinkHeader(
[headers.link, headers['x-amz-meta-link']].join(',')
);

this.scriptRefs = refs
.filter(ref => ref.rel === 'fragment-script')
.slice(0, this.maxAssetLinks)
.map(ref => ref.uri);
this.styleRefs = refs
.filter(ref => ref.rel === 'stylesheet')
.slice(0, this.maxAssetLinks)
.map(ref => ref.uri);
if (refs.length > 0) {
[this.scriptRefs, this.styleRefs] = getFragmentAssetUris(
refs,
this.maxAssetLinks
);
}

if (!isFallback) {
this.emit('response', statusCode, headers);
}

this.insertStart();

Expand Down
51 changes: 51 additions & 0 deletions lib/parse-link-header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';
/**
* Parse link headers
* '<http://example.com/script.js>; rel="fragment-script"'
*
* [
* {
* rel: "fragment-script",
* uri: "http://localhost:8080/script.js"
* }
* ]
*
* Based on code from parse-link-header!
* https://github.com/thlorenz/parse-link-header/blob/master/index.js
*/
module.exports = function parseLinkHeader(linkHeader) {
const assets = linkHeader
.split(/,\s*</)
.map(link => {
const match = link.match(/<?([^>]*)>(.*)/);
if (!match) {
return null;
}
const linkUrl = match[1];
const parts = match[2].split(';');
parts.shift();
return {
uri: linkUrl,
rel: getRelValue(parts[0])
};
})
.filter(v => v && v.rel != null)
.reduce((acc, curr) => {
return acc.concat(curr);
}, []);

return assets;
};

/**
* Get the value of rel attribute
*
* rel="fragment-script" -> ["rel", "fragment-script"]
*/
function getRelValue(parts) {
const m = parts.match(/\s*(.+)\s*=\s*"?([^"]+)"?/);
if (!m) {
return null;
}
return m[2];
}
7 changes: 3 additions & 4 deletions lib/request-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ module.exports = function processRequest(options, request, response) {

// Make resources early discoverable while processing HTML
const assetsToPreload = getFragmentAssetsToPreload(
headers,
fragment.styleRefs,
fragment.scriptRefs,
request.headers
);

Expand Down Expand Up @@ -218,9 +219,7 @@ module.exports = function processRequest(options, request, response) {

resultStream.once('error', handleError);

parsedTemplate.forEach(item => {
resultStream.write(item);
});
parsedTemplate.forEach(item => resultStream.write(item));
resultStream.end();
})
.catch(err => {
Expand Down
31 changes: 17 additions & 14 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use strict';

const LinkHeader = require('http-link-header');

const getCrossOrigin = (url = '', host = '') => {
if (url.includes(`://${host}`)) {
return '';
Expand Down Expand Up @@ -39,21 +37,11 @@ const getLoaderScript = (amdLoaderUrl, { host } = {}) => {
};

// Early preloading of primary fragments assets to improve Performance
const getFragmentAssetsToPreload = (headers, { host } = {}) => {
const getFragmentAssetsToPreload = (styleRefs, scriptRefs, { host } = {}) => {
let assetsToPreload = [];

const { refs = [] } = LinkHeader.parse(
[headers.link, headers['x-amz-meta-link']].join(',')
);
const scriptRefs = refs
.filter(ref => ref.rel === 'fragment-script')
.map(ref => ref.uri);
const styleRefs = refs
.filter(ref => ref.rel === 'stylesheet')
.map(ref => ref.uri);

// Handle Server rendered fragments without depending on assets
if (!scriptRefs[0] && !styleRefs[0]) {
if (scriptRefs.length === 0 && styleRefs.length === 0) {
return assetsToPreload;
}

Expand All @@ -80,6 +68,20 @@ const getFragmentAssetsToPreload = (headers, { host } = {}) => {
return assetsToPreload;
};

function getFragmentAssetUris(refs, assetSize) {
Copy link
Contributor

@ruiaraujo ruiaraujo Aug 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why put this in utils, is it used anywhere else?

Are we planning to add this to the exported API?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, I can move inside fragment.js

const scriptUris = [];
const styleUris = [];

for (const ref of refs) {
if (ref.rel === 'fragment-script') {
scriptUris.push(ref.uri);
} else if (ref.rel === 'stylesheet') {
styleUris.push(ref.uri);
}
}
return [scriptUris.slice(0, assetSize), styleUris.slice(0, assetSize)];
}

const nextIndexGenerator = (initialIndex, step) => {
let index = initialIndex;

Expand All @@ -94,5 +96,6 @@ module.exports = {
getCrossOrigin,
getFragmentAssetsToPreload,
getLoaderScript,
getFragmentAssetUris,
nextIndexGenerator
};
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"author": "Andrey Kuzmin",
"license": "MIT",
"dependencies": {
"http-link-header": "^0.8.0",
"opentracing": "^0.14.3",
"parse5": "^3.0.3",
"util.promisify": "^1.0.0"
Expand Down
49 changes: 49 additions & 0 deletions tests/parse-link-header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const parseLinkHeader = require('../lib/parse-link-header');
const assert = require('assert');

describe('Parse Link Header', () => {
it('returns uri and rel of the passed header', () => {
const linkHeader =
'<http://a.com/app.js>; rel="script",<http://a.com/app.css>; rel="stylesheet"';

assert.deepStrictEqual(parseLinkHeader(linkHeader), [
{ uri: 'http://a.com/app.js', rel: 'script' },
{ uri: 'http://a.com/app.css', rel: 'stylesheet' }
]);
});

it('ignore attributes other than rel and uri', () => {
const linkHeader =
'<http://a.com/app.js>; rel="script"; crossorigin="anonymous"';

assert.deepStrictEqual(parseLinkHeader(linkHeader), [
{ uri: 'http://a.com/app.js', rel: 'script' }
]);
});

it('filters invalid header links', () => {
const linkHeader = 'http://a.com/app.js; rel="script"';

assert.deepStrictEqual(parseLinkHeader(linkHeader), []);
});

it('filters invalid rel attributes', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is an invalid rel attr?
Empty? Can I use in rel="styleshit"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ofcourse you can lol. its valid. But wont be loaded because of the check in getFragmentAssetUris.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the test if for rel="" and no rel attribute at all. do you have any better naming suggestion if its confusing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it could be rephrased... but it is fine.

const linkHeader =
'<http://a.com/app.js>; rel="script";, <http://a.com/app1.css>; rel="stylesheet", <http://a.com/app2.css>;, <http://a.com/app3.css>; rel=""';

assert.deepStrictEqual(parseLinkHeader(linkHeader), [
{ uri: 'http://a.com/app.js', rel: 'script' },
{ uri: 'http://a.com/app1.css', rel: 'stylesheet' }
]);
});

it('do not modify query parms in link urls', () => {
const linkHeader = '<http://a.com/app.js?nocache=1>; rel="script";';

assert.deepStrictEqual(parseLinkHeader(linkHeader), [
{ uri: 'http://a.com/app.js?nocache=1', rel: 'script' }
]);
});
});
1 change: 0 additions & 1 deletion tests/tailor.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,6 @@ describe('Tailor', () => {
mockTemplate.reset();
withFile.close(done);
});

it('should preload external module loader if fragment is present', done => {
nock('https://fragment')
.get('/1')
Expand Down
4 changes: 0 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -878,10 +878,6 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"

http-link-header@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/http-link-header/-/http-link-header-0.8.0.tgz#a22b41a0c9b1e2d8fac1bf1b697c6bd532d5f5e4"

http-signature@~1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
Expand Down