Skip to content

Instantly share code, notes, and snippets.

@rmurphey
Last active August 29, 2015 14:19
Show Gist options
  • Save rmurphey/6842b3b1b806dd123676 to your computer and use it in GitHub Desktop.
Save rmurphey/6842b3b1b806dd123676 to your computer and use it in GitHub Desktop.
Proposed: JS Static Resources Service

Problem Statement

Bazaarvoice has multiple consumer-facing web applications; each of these apps uses some set of vendor resources -- for example, jQuery or Backbone. It is possible for multiple applications to be on the same page; when this happens, each application brings its own vendor resources. This means that Bazaarvoice applications may end up loading the same resource multiple times on the same page, which has a negative impact on performance.

Proposal

Consumer-facing web applications created by Bazaarvoice should be able to share vendor code. To achieve this, we would create the following pieces, discusssed in detail below:

  • A static resources service (e.g. display.static.bazaarvoice.com)
  • A client-side JS module for requesting vendor code from the service, intended for inclusion in the scout file
  • A Grunt (or similar) tool for populating the service

The Static Resources Service

The Static Resources Service would provide a /vendor/*.js endpoint for requesting vendor modules. This endpoint would allow a consumer application to request multiple vendor modules, e.g. /vendor/backbone@1.1+underscore@2.4. It would return a file containing all requested resources, exposed via the predefined callback method BV._vendor.define.

(function () {

BV._vendor.define('underscore@2.4', [], function () {
  return { /* ... */ };
});

BV._vendor.define('backbone@1.1', [ 'underscore', 'jquery' ], function (_, $) {
  return { /* ... */ };
});

}());

Implementation

The service would be implemented as an s3 bucket with Akamai or CloudFront CDN services. See below for details on how the service would be populated.

The Client-Side JS Module

Applications would use a client-side JS module to request vendor modules from the service.

The module would provide an interface such as the following:

BV._vendor.require(['jquery@1.8', 'backbone@1.1', 'underscore@2.4'], function ($, Backbone, _) {
  // ...
});

Given this request, the module would:

  • Determine whether the modules were already available, by inspecting window.BV._vendor.
  • Create a canonical URL for requesting unavailable resources from the service, ordering the requested modules alphabetically in order to facilitate caching:
https://display.static.bazaarvoice.com/vendor/backbone@1.1+underscore@2.4.js
  • Use the response from the service to call the provided callback

The module would be responsible for defining the BV._vendor.register callback that the service uses in its response.

It may be advisable for the module to maintain a list of known-good package names, in order to prevent errors.

Populating the Service

The service would be populated with static files generated via a Grunt-based tool (or similar). This tool would consume a configuration such as the following:

var packages = [
  [ 'jquery@1.8', 'backbone@1.1', 'lodash@2.4' ],
  // ...
];

In the near term, teams would contribute to this configuration (via pull request) to express the dependencies they would need. Importantly, the tool would not just build the exact combinations requested; it would also build all subsets of each combination. So, if the packages array contained an entry for [ 'jquery@1.8', 'backbone@1.1', 'lodash@2.4' ], then the tool would create files for the following (always ordering the modules alphabetically):

  • backbone@1.1,jquery@1.8,lodash@2.4
  • backbone@1.1,jquery@1.8
  • jquery@1.8,lodash@2.4
  • backbone@1.1,lodash@2.4
  • jquery@1.8
  • backbone@1.1
  • lodash@2.4

This covers the case where an application requests ['backbone@1.1', 'jquery@1.8', 'lodash@2.4'] but the loader discovers that one of the resources is already available, and thus only loads the two unavailable resources.

The tool would be responsible for maintaining a mapping of module/version to the code to be included. In most cases, this mapping would simply point to an NPM module; in some cases, it might point to a private Bazaarvoice repo containing an NPM module.

Usage Notes

  • Applications should only make one request to the service per page load.
  • The request to the service should happen from the application's scout file whenever possible, in order to ensure the vendor resources arrive as soon as possible.
  • Applications would be responsible for handling failure cases, timeouts, etc.

Discussion

Some areas require further consideration and discussion:

  • What should the TTL be for these files? We should design the system such that the TTL can be extremely long.
  • How should we handle vendor files that have been modified by Bazaarvoice in order to address issues? We should determine naming and versioning strategies for these files (and avoid creating them at all when possible).
  • It's likely that the service should exist in QA as well as in PROD. I don't think there's a need for a STG service but I'm open to opinions.
  • This service could be used by non-consumer-facing applications as well.
@lawnsea
Copy link

lawnsea commented Apr 16, 2015

What should the TTL be for these files? We should design the system such that the TTL can be extremely long.

If version numbers are in the URL, I think we can set the TTL to be effectively indefinite.

@lawnsea
Copy link

lawnsea commented Apr 16, 2015

How should we handle vendor files that have been modified by Bazaarvoice in order to address issues? We should determine naming and versioning strategies for these files (and avoid creating them at all when possible).

Strawman: backbone@1.1.bv-123

@lawnsea
Copy link

lawnsea commented Apr 16, 2015

It's likely that the service should exist in QA as well as in PROD. I don't think there's a need for a STG service but I'm open to opinions.

Can you expand on this a bit? It's not obvious to me why we would need multiple environments, but I haven't thought about it deeply.

@lawnsea
Copy link

lawnsea commented Apr 16, 2015

How do we handle libraries like Backbone that have a dependency on another vendor library (Underscore)?

@lawnsea
Copy link

lawnsea commented Apr 16, 2015

Nit: the URL should end in .js

@rmurphey
Copy link
Author

My main rationale for a QA version is that it's probably easier in the long run to have it than to not, if only so QA can be where we make mistakes and have to bust the cache, have shorter TTL rules, etc. Just having versions in the URL "should" make a QA instance irrelevant, but ...

@joeslice
Copy link

You can probably consume the CloudFront logs for a few things:

  • Watch for missing combinations and figure out what needs to be generated
  • See what combinations are no longer used -- it would be nice to
  • Harvesting a debugging id (QS parameter just to indicate who is calling)

The tradeoff between TTL and code-resiliency is problematic. It's tempting to have a super high TTL, but we know there will be bugs in all code, including this packaging code. A hard question is defining where that inflection point is.

Dev story

  • What's the story for a developer who wants to upgrade a version of a library to test something but hasn't really decided yet to use it in production? It would be a bit odd/out of expectations for them to have to issue a pull request to this repo in order to try a new function in lodash.
  • What about when a developer wants to debug using source? Will you have a -min and not-min version?

Supported libraries

  • There would be a benefit to having a list of supported/known libraries and versions published somewhere.
  • Who will be responsible for keeping the "source libraries" list up to date? If an application developer wants to use a new/patched version of a library, what's the workflow?
  • The combination of files can really get out of hand, we have to figure out a limiting factor ("we will stop supporting version <= 19.22.x of jquery at this date" or so).

@benhollander
Copy link

  • Do we have an effective strategy to identify libraries that are being shared by multiple apps?
  • How do we decide which modules live in this service as opposed to just being within a particular app? Is it only a function of duplication, or should we consider including other vendor code in this service as well?

@markmarkoh
Copy link

I'm back and forth on whether I like this or not.

I'd think there is a good chance this approach in it's current form may only slightly curb the problem in the original statement:
when this happens, each application brings its own vendor resources.

If my app specifies backbone@1.1,lodash@2.4 and your app specifies backbone@1.1.2,lodash@2.3, there was no gain by this approach, since we'd need different versions of the same library.

I don't think that's a contrived example - I have no idea what version of BB or lodash Firebird is currently using (I looked in the repo and couldn't find it easily, not that I want to have to know ).

I fear the awkwardness of "Hey curations, FB just went to lodash 2.3.5 for a bug fix, mind upgrading to that version to so we can get the performance gains back that we lost when they upgraded and stop double loading nearly identical versions of lodash" tickets popping up in our queues, and the unnecessary deployments to follow.

Perhaps it's an easy to solve that problem by introducing the x placeholder into the dep versions, for instance:

[backbone@1.x,lodash@2.x] or [backbone~1.x]

Given that the libraries that we'll support will version themselves in compliance with semver, it might lead to less whack-a-moleing of matching versions across projects, but an increased risk in breaking something. We could be pretty vocal about changes to latest on libraries ("On July 1 2015 backbone 1.x will point at 1.2.4").

@markmarkoh
Copy link

I wonder if this can be NPM backed instead of custom-s3 bucket backed. Instead of BV stating "We support the following 5 libraries", just support anything in NPM and have our service grab the static resources from NPM (preferably once) and cache that version.

That might be a more universally usable service (not that we have an obligation for this to be anything more than a custom solution for BV).

It will also answers Ben's question above - all vendor code would be fetched by this service.

@reason-bv
Copy link

If NPM-based we'd absolutely need to be running our own proxy registry - NPM isn't stable enough to be relied upon, newly funded or not.

There is a strong incentive to craft something here that doesn't use any servers that we must maintain. As soon as you add any of those it starts to tip the balance towards not worth the effort.

@rmurphey
Copy link
Author

Thanks for the fantastic feedback, this has been a great discussion so far. Here's where I am with this right now:

  • After discussion with various folks, it's clear we need to maintain BV-specific vendor code -- for example, Firebird has had to make BV-specific patches to Backbone and jQuery (and those patches should really be used by all 3pjs apps at BV). Along with the reasons that Reason cited, this takes NPM off the table, and I'm actually OK with that. We can maintain a repo with supported vendor modules/versions in it; that repo can follow along with the public versioning, but tack a -bv on the end perhaps. This actually makes the implementation a lot easier, I think.
  • We don't have great visibility right now into the versions of vendor code that various BV projects are using. That means that yes, to start with, there would be minimal bang for the buck -- though there are certainly benefits to making vendor code separate and more cacheable than application code. The roadmap here is to build the system (I think this is actually one of the easier ideas we've had!); get Curations, Firebird, and Spotlights using it; and then work to reconcile the versions they each use. My hope is that the potential savings of 10s of K will be compelling.
  • Anything that is unlikely to change often, and potentially shareable among projects, is a candidate for inclusion in this service -- honestly, it's not just vendor code.

@rmurphey
Copy link
Author

Here are the versions I found for a few different libraries:

jQuery

Spotlights: Not used
Curations Templates: 1.8.3 with modifications
Firebird: 1.11.1 with modifications

Backbone

Spotlights: 1.1.2 with modifications
Curations Templates: unused
Firebird: 1.0.0 with modifications

Moment.js

Curations Templates: 2.6.0
Firebird: 1.7.2
Spotlights: Not (yet) used

Lodash/Underscore

Firebird: Lodash 1.2.0 with modifications
Curations Templates: Underscore 1.5.2
Spotlights: Lodash 2.4.1

@lawnsea
Copy link

lawnsea commented Apr 20, 2015

@joeslice said:

What about when a developer wants to debug using source? Will you have a -min and not-min version?

Good question. We should provide source maps for all files.

@msmolev
Copy link

msmolev commented May 11, 2015

Should we think about plugging PRR/Agrippa into this system as well? I know they use jQuery with modifications, so theoretically that could be one less jQuery (but then... are all "with modifications" automatically mean "not compatible with what other apps want"?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment