Giter Site home page Giter Site logo

slimmer's Introduction

Slimmer

Slimmer provides Rack middleware for applying a standard header and footer around pages returned by a Ruby (Rack) application.

It does this by taking the page rendered by the application, extracting the contents of a div with id 'wrapper' and inserting that into a div with the same id in one of its templates. It also transfers various other details, such as meta, script, and style tags.

View documentation

Use in a Rails app

Slimmer provides a Railtie so no configuration is necessary.

Caching

Slimmer makes HTTP requests to static for templates. These are cached using Rails.cache.

Asset tag helpers

To get asset tag helpers to point to your external asset server, add

config.action_controller.asset_host = "http://my.alternative.host"

to application.rb.

Specifying a template

A specific template can be requested by giving its name in the X-Slimmer-Template HTTP header.

In a controller action, you can do this by calling slimmer_template.

class MyController < ApplicationController
  def index
    slimmer_template 'homepage'
  end
end

There's also a macro style method which will affect all actions:

class YourController < ApplicationController
  slimmer_template :admin
end

To get this, include Slimmer::Template in your ApplicationController:

class ApplicationController < ActionController::Base
  include Slimmer::Template
end

Use in before_action renders

If you have a non-default layout and want to render in a before_action method, note that you may have to explicitly call slimmer_template(:your_template_name) in the action before rendering. Rendering in a before_action immediately stops the action chain, and since slimmer usually calls slimmer_template as an after_action, it would be skipped over (and you'd get the default layout).

Logging

Slimmer can be configured with a logger by passing in a logger instance (anything that quacks like an instance of Logger). For example, to log to the Rails log, put the following in an initializer:

YourApp::Application.configure do
  config.slimmer.logger = Rails.logger
end

Note: This can't be in application.rb because the Rails logger hasn't been initialized by then.

Debug logging

By default if you pass in a logger with its log level set to debug, slimmer will dup this logger and reduce the level to info. (Slimmer's debug logging is very noisy). To prevent this, set the enable_debugging option to true. e.g. for Rails:

YourApp::Application.configure do
  config.slimmer.enable_debugging = true
end

Cucumber

Add the following code to features/support:

require 'slimmer/cucumber'

RSpec

Add the following code to spec/spec_helper:

require 'slimmer/rspec'

Licence

MIT License

slimmer's People

Contributors

36degrees avatar alext avatar andrewgarner avatar barrucadu avatar bradwright avatar chrisbashton avatar chrisroos avatar craigw avatar danacotoran avatar dependabot[bot] avatar dhwthompson avatar dsingleton avatar edds avatar fofr avatar gpeng avatar heathd avatar injms avatar jamiecobbett avatar jordanhatch avatar jystewart avatar kevindew avatar lazyatom avatar murilodalri avatar nickcolley avatar techbelly avatar tetrino avatar thomasleese avatar threedaymonk avatar tijmenb avatar tomafro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

slimmer's Issues

Moving scripts into the page `<head>`

Hypothesis

By moving scripts from the end of the DOM (before the closing </body> tag), and setting the defer attribute on scripts, this will improve page performance.

Theory

In some pages on GOV.UK a chart from WebPageTest shows there's a significant lag in the discovery of <script> tags since they are located at the very bottom of the DOM. They are the last elements to be streamed to the browser.

Waterfall of potential savings by moving the script discovery forwards.

As seen in the above, by moving the scripts into the head we will bring the discovery of these scripts further forwards in the waterfall. Discovery of assets allows them to be requested and downloaded sooner.

Script Execution

Because the scripts are in the <head> the DOM isn't fully downloaded and parsed ready for the script execution. We have 2 options, use of either the async or defer attribute:

  • async - this attribute will download the script asynchronously alongside the HTML, but will execute as soon as download completes (and in any order compared to other scripts).
  • defer - this attribute will download the script in parallel, but execution will only happen once the DOM is complete and the domInteractive event fires.

In theory the parallel downloading of the script and HTML should improve page performance.

Current GOV.UK setup

Slimmer currently has a processor that moves all the script tags found in the page to just before the closing </body> tag. This processor can be seen here. It may be possible to reverse this move either for all scripts, or only selected scripts.

Testing

Thankfully it is possible to test the theory above without the need to touch any production code by using a Cloudflare Worker and the HTML Rewriter API.

Worker setup

With the worker we will be rewriting the HTML 'on-the-fly' as the request is made. We will be:

  1. Removing some inline scripts from the page (they can't use defer)
  2. Removing the JavaScript references from the bottom of the page
  3. Adding the JavaScript back into the <head> and appending the defer attribute.
  4. Add the inline scripts back into the page by creating a custom response to a specific url.pathname

A request passed through the above worker will seem completely normal to the browser who will parse and execute the page accordingly.

WebPageTest setup

Using WebPageTest we can test the performance of our modified page against the baseline setup. Thus allowing us to see if it improves or makes performance worse. To do this we must use the overrideHost feature available in WebPageTest:

overrideHost www.gov.uk govuk-worker-example.workers.dev

This code essentially says "any request to www.gov.uk from the original page route through the worker and therefore modify the response presented back to WebPageTest.

The full WebPageTest can be seen below:

setCookie https://www.gov.uk/ cookies_policy={"essential":true,"settings":true,"usage":true,"campaigns":true}
setCookie https://www.gov.uk/ cookies_preferences_set=true
setCookie https://www.gov.uk/ global_bar_seen={"count":999,"version":8}
addHeader x-bypass-transform:true
overrideHost www.gov.uk govuk-worker-example.workers.dev
navigate %URL%

Note: govuk-worker-example.workers.dev isn't a real domain, just an example placeholder.

The x-bypass-transform:true header allows us to bypass the transformation and capture a baseline that itself is passed through the worker but unmodified.

Cookies have been set in order to accept the cookie banner for all tests.

Page setup

The News and Communication page was chosen as it displayed the issue from the original hypothesis, and also has a large DOM structure. So the issue could be clearly seen. It is possible to use any page, but because of the nature of the workers and how they modify the HTML using CSS selectors, they can be quite flakey if you are relying on the basic ordering of elements on a page e.g. removing the 7th script element on a page (body > script:nth-of-type(7)).

Browser setup

I decided to test pages in Chromium under a 3G Fast connection on a desktop device and also a real Moto G4 device (located in Dulles, USA). This should give us more of an idea as to "real world" performance.

Results Chrome Desktop - 3G Fast

Baseline (run through worker, no transform)

Baseline waterfall as it is seen without modifications.

Chrome Desktop (run through worker, with HTML transforms applied)

The page load after transformation.

Comparing the charts above it is possible to see the JS has been shifted forwards, and in doing so it is requested in the middle of the highest priority CSS. The CSS and JS will now be competing for limited bandwidth (3G Fast connection).

Visual Impact

Here we compare both tests with each other:

Resulting load filmstrip across both tests.

Visual progress graph for both tests.

As you can see from the filmstrip and the visual progress there's been some improvement to the rendering of the font, but nothing substantial under these conditions. Looking at the visual progress graph we can actually see that the baseline started to render sooner than our transformed version.

Results Moto G4 Mobile - 3G Fast

Baseline (run through worker, no transform)

The waterfall for a Moto G4 at the baseline

Chrome Desktop (run through worker, with HTML transforms applied)

The waterfall for a Moto G4 once transformed with defer.

In these two tests we can see in both cases that the requests all start at a similar time, but the real difference is the interleaving between CSS & JS once the scripts are moved into the head with the defer attribute. At this point these resources are competing for limited bandwidth.

Visual Impact

Here we compare both tests with each other:

Resulting load filmstrip across both tests.

Visual progress graph for both tests.

As you can see above the impact of moving the scripts to the <head> and adding defer has made a huge negative impact on performance. First paint is 500ms slower after modifications to the page are applied. A device with limited memory / CPU and bandwidth is having share these resources the best it can. The bandwidth being allocated between CSS and JS delays the download and parsing of all the CSS, which in turn delays the creation of the render tree. No render tree = nothing painted to the screen.

Conclusion

From these two sets of tests it is clear to see that it isn't just as clear cut as moving scripts into the head and adding defer will always result in improved performance. In fact in some cases it looks to actually hinder performance.

Recommendation

My recommendation off the back of this testing is to keep the tag_mover.rb processor in place, but factor in some additional functionality to allow developers to exclude certain scripts if required. E.g. via the addition of a data attribute. This would then give the flexibility to keep certain scripts in the head when required, and also mean the current functionality isn't broken.

Rails 4 compatibility

Hi,

I've just tried to include Slimmer in a Rails 4 project, and it seems the Railtie isn't getting loaded properly. I had to manually add the config to application.rb like so:

config.middleware.use("Slimmer::App", :asset_host => Plek.new.find("static"))

We're going to do some more investigation this end, but thought I'd flag it up in case there was anything obvious I'm missing

Parsing with Nokogiri::HTML defaults to HTML4 and produces invalid markup

When attempting to use <picture><source .../></picture> the parser will auto close the <source .../> tag to <source></source> which is invalid HTML.

We could switch from using Nokogiri::HTML to Nokogiri::HTML5 but this will have implications to investigate for IE11 and screen readers.

A table I found shows that <main> should be ok:

https://www.accessibilityoz.com/2020/02/html5-sectioning-elements-and-screen-readers/

but we need to check with Assistivlabs

Screenshot 2023-01-24 at 4 54 44 pm

Slimmer::Cache is not threadsafe

The implementation of the cache is not threadsafe because the implementation of Hash in Ruby isn't threadsafe. You'll be fine if you're just using MRI but you may experience issues on other rubies like JRuby.

Probably best to use an existing threadsafe cache like ActiveSupport::Cache::MemoryStore or use ThreadSafe::Cache from the thread_safe gem.

Better Component testing helpers

Applications often want to write tests that assert against something in a component, in test mode a component will render a test version, containing the arguments passed to it as JSON (not fetching anything from static).

Currently apps tend to do a substring match on the JSON within the component wrapper, which is a bit hacky. Ideally there would be test helpers that can assert a specific argument was passed to the component, without having to parse the JSON out of the markup in each application.

eg,

assert_component('title', 'context', 'foo')

This requires writing helpers for a set of testing frameworks, but as we standardised our rails frontends that should be easier, and at the least we could add a helper for the most common one.

Heres an example for an application implementing these helpers itself: https://github.com/alphagov/government-frontend/blob/master/test/test_helper.rb#L24-L83

It would be nice if this was re-usable in other apps.

Nokogiri deprecation warnings

Deploying licencefinder, I can see deprecation warnings from Nokogiri called from Slimmer:

/data/vhost/licencefinder/shared/bundle/ruby/2.7.0/gems/slimmer-16.0.0/lib/slimmer/processors/metadata_inserter.rb:22: warning: Passing a Node as the second parameter to Node.new is deprecated. Please pass a Document instead, or prefer an alternative constructor like Node#add_child. This will become an error in a future release of Nokogiri.

Use stale Slimmer cache on static error response

Requests to static for templates will sometimes fail, due to network blips. A failed request will almost certainly succeed the next time. We see this in application's errbit logs at constant low levels.

In that situation we should fall back to the stale cache (if it exists), rather than raising an error and showing the user a 500. It's better to use the out of date* version and return a 200.

*older than 5 minutes, in 99% of cases the response from static will be the same.

License missing from gemspec

Some companies will only use gems with a certain license.
The canonical and easy way to check is via the gemspec,

via e.g.

spec.license = 'MIT'
# or
spec.licenses = ['MIT', 'GPL-2']

Even for projects that already specify a license, including a license in your gemspec is a good practice, since it is easily
discoverable there without having to check the readme or for a license file. For example, it is the field that rubygems.org uses to display a gem's license.

For example, there is a License Finder gem to help companies ensure all gems they use
meet their licensing needs. This tool depends on license information being available in the gemspec. This is an important enough
issue that even Bundler now generates gems with a default 'MIT' license.

If you need help choosing a license (sorry, I haven't checked your readme or looked for a license file), github has created a license picker tool.

In case you're wondering how I found you and why I made this issue, it's because I'm collecting stats on gems (I was originally looking for download data) and decided to collect license metadata,too, and make issues for gemspecs not specifying a license as a public service :).

I hope you'll consider specifying a license in your gemspec. If not, please just close the issue and let me know. In either case, I'll follow up. Thanks!

p.s. I've written a blog post about this project

Rename `Slimmer::SharedTemplates`

When I first saw include Slimmer::SharedTemplates, I didn't know that it had something to do with the govuk-components.

Perhaps Slimmer::GovukComponents would be more self-documenting.

README out-of-date

It looks like template_path and template_host options are no longer supported and some other things have changed or been added. It would be nice if the README was updated accordingly.

Webmock only listed as development dependency

We require webmock in the component test helpers here:

This gets included when a host application loads the slimmer test helpers:
https://github.com/alphagov/slimmer#rspec

require 'slimmer/test_helpers/govuk_components'

But we only define webmock as a development dependency:

s.add_development_dependency 'webmock', '1.18.0'

A host application without webmock errors with:

active_support/dependencies.rb:293:in `require': cannot load such file -- webmock (LoadError)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.