Unholy Rails: External Scripts, jQuery Plugins, and Page-Specific JavaScript(railsapps.github.com)
railsapps.github.com
Unholy Rails: External Scripts, jQuery Plugins, and Page-Specific JavaScript
http://railsapps.github.com/rails-javascript-include-external.html
6 comments
That's the so-called Garber-Irish technique that I briefly mention in the article. I want to investigate it further. I'm not sure whether to use classes or ids in the body tag or try the data-* attributes he suggests.
Also I'm a bit reluctant to use page-specific CSS selectors. It seems to be accepted practice (Modernizr and other libraries) but still seems like a hack. After all, there is never more than one body tag so it seems inconsistent with the HTML spec to apply a class or id to it.
Also I'm a bit reluctant to use page-specific CSS selectors. It seems to be accepted practice (Modernizr and other libraries) but still seems like a hack. After all, there is never more than one body tag so it seems inconsistent with the HTML spec to apply a class or id to it.
The data attributes work great. They can fool you when you're not looking out for specific sitatuons. Like a typical error page will be show create or update action and not edit or new.
The killer feature for me is being able to organize my coffeescript code into various classes and execute it all on one page. Makes it much easier to get other developers up to speed on the javascript code.
I'm still looking for a way not to load specific javascript files from the asset pipeline and not litter my views with content_for :head. It's better all in one place.
The killer feature for me is being able to organize my coffeescript code into various classes and execute it all on one page. Makes it much easier to get other developers up to speed on the javascript code.
I'm still looking for a way not to load specific javascript files from the asset pipeline and not litter my views with content_for :head. It's better all in one place.
CSS selectors should describe visual design, not content. Selector chains like .landing-page #buy-button are a CSS code smell. Unfortunately this seems to be more common than not, in my experience.
What? IDs and classes are part of the HTML, not the CSS, and as such, they should specify semantics, not presentation. If a class or ID specifies presentation, like .red-button, you have to change the HTML when the design changes, not just the CSS.
Sorry, just generally ranting, not in your direction. :)
My personal (strong) preference is to use semantic IDs as JS hooks (or just for markup readability), and keep all class names content-agnostic. Obviously there will be cases where you need to use (CSS) classes in your JS as well, so a good practice is to prefix them with js- and not base any style definitions on them. Separation of concerns and all that. You should be able to refactor your CSS, markup, and JS independently and not have anything break.
Additionally, you should be able to copy paste a chunk of markup and have it look identical no matter where you put it, which definitely won't be the case if you are using long, content-referencing selector chains. Keeping your CSS flat, low level, and generic increases reusability and prevents CSS bloat, which can quickly get out of hand even in a medium sized app. Of course, all this is probably overkill if you are making small, static sites.
My personal (strong) preference is to use semantic IDs as JS hooks (or just for markup readability), and keep all class names content-agnostic. Obviously there will be cases where you need to use (CSS) classes in your JS as well, so a good practice is to prefix them with js- and not base any style definitions on them. Separation of concerns and all that. You should be able to refactor your CSS, markup, and JS independently and not have anything break.
Additionally, you should be able to copy paste a chunk of markup and have it look identical no matter where you put it, which definitely won't be the case if you are using long, content-referencing selector chains. Keeping your CSS flat, low level, and generic increases reusability and prevents CSS bloat, which can quickly get out of hand even in a medium sized app. Of course, all this is probably overkill if you are making small, static sites.
I wrote the article to explore what to do when your application is not wholly Rails. I got a lot of help and advice but I don't think there's full agreement on recommended practices yet. I've seen a range of opinion but a lot of advice doesn't hold up to analysis. The article reflects the best advice I've gotten.
Well thank you for compiling all of this together because about to deploy a new version of my product that has a significant amount of fairly complex JS and I found it very frustrating to not have any goto resource in regards to structuring and optimizing the code. I'm going to try and incorporate some of your tips and will report back if you'd like
You're welcome. Take a look at the link (in the article) to Ken Collins’s "Secrets Of The Asset Pipeline" slide deck. It's got some great recommendations about organizing your JS code. Let me know how it goes (you can leave comments).
I dislike the approach described where one concatenates every single javascript file into application.js and then tests for the presence of particular dom elements or classes within each file. This taxes the creation of new scripts somewhat as you must ensure you're not accidentally creating a dom element or class which is going to enable some undesired scripts. Another possible option with "require_tree ." is to use a framework like http://requirejs.org/ and then use in line script tags within pages to require a particular module.
I also think it's a bad idea to enable asset compilation in production. From the Rails asset guides (http://guides.rubyonrails.org/asset_pipeline.html#live-compi...):
"This mode uses more memory, performs more poorly than the default and is not recommended."
My advice would be to use application.js to concatenate scripts you're highly likely to use on every page, e.g., jquery, bootstrap, etc. Then organize the rest of your page specific scripts into app/assets/javascripts/<controller>/<action>.js<.extensions> and add a `javascript_include_tag "#{params[:controller]}/#{params[:action]}` in any views that rely on page specific scripts (within a content_for(:head) block). This way you'll likely have 2 local scripts loading for each page - the application.js file and any page specific script. The upsides are that you don't need to worry about undesired javascript running, the application.js file will be cached and reused across all pages, and your lightweight page specific js file will be served the first time a user loads the page then cached with every subsequent visit. The potential downside is that you either need to specify a precompile array by hand in your environment specific config file or automatically glob files to be precompiled.
Note that you can also use the manifest declarations inside of your regular javascript files, e.g., `//= require 'backbone'` at the top of one of your page specific javascript files.
I also think it's a bad idea to enable asset compilation in production. From the Rails asset guides (http://guides.rubyonrails.org/asset_pipeline.html#live-compi...):
"This mode uses more memory, performs more poorly than the default and is not recommended."
My advice would be to use application.js to concatenate scripts you're highly likely to use on every page, e.g., jquery, bootstrap, etc. Then organize the rest of your page specific scripts into app/assets/javascripts/<controller>/<action>.js<.extensions> and add a `javascript_include_tag "#{params[:controller]}/#{params[:action]}` in any views that rely on page specific scripts (within a content_for(:head) block). This way you'll likely have 2 local scripts loading for each page - the application.js file and any page specific script. The upsides are that you don't need to worry about undesired javascript running, the application.js file will be cached and reused across all pages, and your lightweight page specific js file will be served the first time a user loads the page then cached with every subsequent visit. The potential downside is that you either need to specify a precompile array by hand in your environment specific config file or automatically glob files to be precompiled.
Note that you can also use the manifest declarations inside of your regular javascript files, e.g., `//= require 'backbone'` at the top of one of your page specific javascript files.
I like your suggestion of using RequireJS. I'll add that to the next revision of the article.
Your strategy of using two JS scripts per view, one site-wide and one page-specific is interesting. I wonder how to test the performance to really determine the value. Maybe look at time-to-render?
Your strategy of using two JS scripts per view, one site-wide and one page-specific is interesting. I wonder how to test the performance to really determine the value. Maybe look at time-to-render?
I do this as well. I break apps into "components" -- each component has it's own CSS and JavaScript file. A component covers a logical domain, such as administrative functions, or a reporting piece.
Then I have an application level CSS&JS that has everything generic in it. This is for a reasonably big app, so the overhead of having a single CSS/JS isn't insignificant - particularly when supporting mobile devices.
The other useful thing is a I as a dasherized version of the controller name and action to the body tag - as an id and a class respectively. So the body tag is something like <body id='my-controller' class='show'>
I also expose these as JavaScript globals CONTROLLER and ACTION (I also have LOCALE and COMPONENT).
These constructs are to be used sparingly -- you don't want to mash everything together too much -- but they're very handy when used right.
Then I have an application level CSS&JS that has everything generic in it. This is for a reasonably big app, so the overhead of having a single CSS/JS isn't insignificant - particularly when supporting mobile devices.
The other useful thing is a I as a dasherized version of the controller name and action to the body tag - as an id and a class respectively. So the body tag is something like <body id='my-controller' class='show'>
I also expose these as JavaScript globals CONTROLLER and ACTION (I also have LOCALE and COMPONENT).
These constructs are to be used sparingly -- you don't want to mash everything together too much -- but they're very handy when used right.
Yes I guess that would be the right metric. You'd need to look at average time to render over a number of deploys with both strategies. I'm pleased to see articles approaching this subject as there's definitely not enough solid discussion around this.
One other optimization you may want to talk about in your next article is to use a gem like the asset_sync gem to upload your assets to S3 or CloudFront (or similar) at compile time.
One other optimization you may want to talk about in your next article is to use a gem like the asset_sync gem to upload your assets to S3 or CloudFront (or similar) at compile time.
I'd like to look deeper into this option. I didn't mention it in the article, but @jo_liss suggested hosting all assets on CloudFront in production. She points out that you can set up CloudFront and get your own content-delivery network in five minutes. Once you have your own CDN, there's no advantage to using any 3rd-party JS host.
That's somewhat true. However, using Google does have an advantage over your own CDN -- it's more likely to already be cached in someone's browser.
On these occassions the hit to download JQuery (or something big like JQueryUI) is zero.
This is more a consideration for a landing page than a heavy-use application though.
On these occassions the hit to download JQuery (or something big like JQueryUI) is zero.
This is more a consideration for a landing page than a heavy-use application though.
> I also think it's a bad idea to enable asset compilation in production.
I don't see where the article recommended that. The article (and the Rails Guides) seem to be pre-compiling for production. This is the standard & recommended practice.
I don't see where the article recommended that. The article (and the Rails Guides) seem to be pre-compiling for production. This is the standard & recommended practice.
Yes, the articles and all of us in the comment thread are recommending pre-compiling. There's a rails configuration option that enables run-time compilation of uncompiled assets - this is referred to both in the rails guide and in the linked HN article. It is not recommended for performance reasons.
Hi Finbarr, I'm interested in learning more about this strategy. I've been using the Garber-Irish method (http://viget.com/inspire/extending-paul-irishs-comprehensive...) for running scripts based on controller/actions, but everything is still always served to every page...With your solution, only truly global scripts seems to be served. I'll try to throw up a test app on github to test out what you've done here, but do you have any existing app that uses this paradigm? I'd love to take a closer look.
Please do test it and post the results. It seems a real point of contention whether a single JS script can outperform page-specific scripts. The team behind the Rails assets pipeline has pushed the single script approach. It'll take some testing to lay the debate to rest.
I think there are a number of factors which will affect the outcome, including:
- number of $(document).ready() callbacks that check whether the current file should run within the single application.js approach. This could grow to be an issue with much larger apps.
- how often the application.js file gets invalidated by a deploy (if it is every deploy this may be annoying for users if there's always a large performance penalty on the first download - especially for apps where the usage isn't regular).
- in the 2 scripts version it's very unlikely that the heavyweight application.js global file will get invalidated regularly, but it is likely that several of the smaller specific page js files will get invalidated with each deploy.
I guess the question is whether downloading several smaller files for each deploy beats downloading the whole application.js file for each deploy (network overhead being the likely bottleneck here).
- number of $(document).ready() callbacks that check whether the current file should run within the single application.js approach. This could grow to be an issue with much larger apps.
- how often the application.js file gets invalidated by a deploy (if it is every deploy this may be annoying for users if there's always a large performance penalty on the first download - especially for apps where the usage isn't regular).
- in the 2 scripts version it's very unlikely that the heavyweight application.js global file will get invalidated regularly, but it is likely that several of the smaller specific page js files will get invalidated with each deploy.
I guess the question is whether downloading several smaller files for each deploy beats downloading the whole application.js file for each deploy (network overhead being the likely bottleneck here).
These are good points. I'll experiment. Thanks!
One more thing - if you concatenate all of your javascript files into a single application.js, the cache will be invalidated every time you make a change to any of your javascript files. I think all in all, this is a bad approach.
Good article. Through trial and error, this is the approach that we've taken. My only gripe is that it appears (to me) to be the only way that works, as all my earlier faltering efforts were not successful.
Asset pipeline is great, but it is a pain to get going initially with disparate assets/gems etc. Which, I assume, almost every Rails project has.
Asset pipeline is great, but it is a pain to get going initially with disparate assets/gems etc. Which, I assume, almost every Rails project has.
Awesome writeup! Rails asset management can get pretty complex for newbies, but with a few good strategies like described in the article, it can be tamed and managed without too much trouble.
I'm suprised no mention of RequireJS...the part labeled "JavaScript’s Missing Directive" seems to be specifically gearing up for it.
Good overview though.
Good overview though.
You still have to load in all your js but at least you can execute exactly what you want per controller and action and have it documented all in one place.