[Checkins] SVN: hurry.resource/trunk/ Add support for rendering inclusions into separate top and bottom fragments.
Martijn Faassen
faassen at infrae.com
Mon Oct 13 15:34:48 EDT 2008
Log message for revision 92153:
Add support for rendering inclusions into separate top and bottom fragments.
Changed:
U hurry.resource/trunk/CHANGES.txt
U hurry.resource/trunk/src/hurry/resource/README.txt
U hurry.resource/trunk/src/hurry/resource/__init__.py
U hurry.resource/trunk/src/hurry/resource/core.py
U hurry.resource/trunk/src/hurry/resource/interfaces.py
-=-
Modified: hurry.resource/trunk/CHANGES.txt
===================================================================
--- hurry.resource/trunk/CHANGES.txt 2008-10-13 19:33:38 UTC (rev 92152)
+++ hurry.resource/trunk/CHANGES.txt 2008-10-13 19:34:48 UTC (rev 92153)
@@ -11,6 +11,13 @@
``hurry.resource`` that sets the mode for the current needed
inclusions.
+* Added support for rendering resources into two fragments, one to
+ be included at the top of the HTML page in the ``<head>`` section,
+ the other to be included just before the ``</body>`` section. In
+ some circumstances doing this can `speed up page load time`_.
+
+ .. `speed up page load time`: http://developer.yahoo.net/blog/archives/2007/07/high_performanc_5.html
+
0.1 (2008-10-07)
================
Modified: hurry.resource/trunk/src/hurry/resource/README.txt
===================================================================
--- hurry.resource/trunk/src/hurry/resource/README.txt 2008-10-13 19:33:38 UTC (rev 92152)
+++ hurry.resource/trunk/src/hurry/resource/README.txt 2008-10-13 19:34:48 UTC (rev 92153)
@@ -600,13 +600,160 @@
... adapts=(Library,),
... provides=ILibraryUrl)
-Rendering the inclusions now will will result in the HTML fragment we need::
+Rendering the inclusions now will will result in the HTML fragments we
+need to include on the top of our page (just under the ``<head>`` tag
+for instance)::
>>> print needed.render()
<link rel="stylesheet" type="text/css" href="http://localhost/static/foo/b.css" />
<script type="text/javascript" src="http://localhost/static/foo/a.js"></script>
<script type="text/javascript" src="http://localhost/static/foo/c.js"></script>
+Top and bottom fragments
+========================
+
+It's also possible to render the resource inclusions into two
+fragments, some to be included just after the ``<head>`` tag, but some
+to be included at the very bottom of the HTML page, just before the
+``</body>`` tag. This is useful as it can `speed up page load times`_.
+
+.. _`speed up page load times`: http://developer.yahoo.com/performance/rules.html
+
+Let's look at the same resources, now rendered separately into ``top``
+and ``bottom`` fragments::
+
+ >>> top, bottom = needed.render_topbottom()
+ >>> print top
+ <link rel="stylesheet" type="text/css" href="http://localhost/static/foo/b.css" />
+ <script type="text/javascript" src="http://localhost/static/foo/a.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/c.js"></script>
+ >>> print bottom
+ <BLANKLINE>
+
+There is effectively no change; all the resources are still on the
+top. We can enable bottom rendering by calling the ``bottom`` method before
+we render::
+
+ >>> needed.bottom()
+
+Since none of the resources indicated it was safe to render them at
+the bottom, even this explicit call will not result in any changes::
+
+ >>> top, bottom = needed.render_topbottom()
+ >>> print top
+ <link rel="stylesheet" type="text/css" href="http://localhost/static/foo/b.css" />
+ <script type="text/javascript" src="http://localhost/static/foo/a.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/c.js"></script>
+ >>> print bottom
+ <BLANKLINE>
+
+``bottom(force=True)`` will however force all javascript inclusions to be
+rendered in the bottom fragment::
+
+ >>> needed.bottom(force=True)
+ >>> top, bottom = needed.render_topbottom()
+ >>> print top
+ <link rel="stylesheet" type="text/css" href="http://localhost/static/foo/b.css" />
+ >>> print bottom
+ <script type="text/javascript" src="http://localhost/static/foo/a.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/c.js"></script>
+
+Let's now introduce a javascript resource that says it is safe to be
+included on the bottom::
+
+ >>> y2 = ResourceInclusion(foo, 'y2.js', bottom=True)
+
+When we start over without ``bottom`` enabled, we get this resource
+show up in the top fragment after all::
+
+ >>> needed = NeededInclusions()
+ >>> needed.need(y1)
+ >>> needed.need(y2)
+
+ >>> top, bottom = needed.render_topbottom()
+ >>> print top
+ <link rel="stylesheet" type="text/css" href="http://localhost/static/foo/b.css" />
+ <script type="text/javascript" src="http://localhost/static/foo/a.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/c.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/y2.js"></script>
+ >>> print bottom
+ <BLANKLINE>
+
+We now tell the system that it's safe to render inclusions at the bottom::
+
+ >>> needed.bottom()
+
+We now see the resource ``y2`` show up in the bottom fragment::
+
+ >>> top, bottom = needed.render_topbottom()
+ >>> print top
+ <link rel="stylesheet" type="text/css" href="http://localhost/static/foo/b.css" />
+ <script type="text/javascript" src="http://localhost/static/foo/a.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/c.js"></script>
+ >>> print bottom
+ <script type="text/javascript" src="http://localhost/static/foo/y2.js"></script>
+
+When we force bottom rendering of Javascript, there is no effect of
+making a resource bottom-safe: all ``.js`` resources will be rendered
+at the bottom anyway::
+
+ >>> needed.bottom(force=True)
+ >>> top, bottom = needed.render_topbottom()
+ >>> print top
+ <link rel="stylesheet" type="text/css" href="http://localhost/static/foo/b.css" />
+ >>> print bottom
+ <script type="text/javascript" src="http://localhost/static/foo/a.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/c.js"></script>
+ <script type="text/javascript" src="http://localhost/static/foo/y2.js"></script>
+
+Note that if ``bottom`` is enabled, it makes no sense to have a
+resource inclusion ``b`` that depends on a resource inclusion ``a``
+where ``a`` is bottom-safe and ``b``, that depends on it, is not
+bottom-safe. In this case ``a`` would be included on the page at the
+bottom *after* ``b`` in the ``<head>`` section, and this might lead to
+ordering problems. Likewise a rollup resource shouldn't combine
+resources where some are bottom-safe and others aren't.
+
+The system makes no sanity checks for misconfiguration of
+bottom-safety however; it could be the user simply never enables
+``bottom`` mode at all and doesn't care about this issue. In this case
+the user will want to write Javascript code that isn't safe to be
+included at the bottom of the page and still be able to depend on
+Javascript code that is.
+
+bottom convenience
+==================
+
+Like for ``need`` and ``mode``, there is also a convenience spelling for
+``bottom``::
+
+ >>> request = Request()
+ >>> l1 = ResourceInclusion(foo, 'l1.js', bottom=True)
+ >>> l1.need()
+
+Let's look at the resources needed by default::
+
+ >>> c = component.getUtility(ICurrentNeededInclusions)
+ >>> top, bottom = c().render_topbottom()
+ >>> print top
+ <script type="text/javascript" src="http://localhost/static/foo/l1.js"></script>
+ >>> print bottom
+ <BLANKLINE>
+
+Let's now change the bottom mode using the convenience
+``hurry.resource.bottom`` spelling::
+
+ >>> from hurry.resource import bottom
+ >>> bottom()
+
+Re-rendering will show it's honoring the bottom setting::
+
+ >>> top, bottom = c().render_topbottom()
+ >>> print top
+ <BLANKLINE>
+ >>> print bottom
+ <script type="text/javascript" src="http://localhost/static/foo/l1.js"></script>
+
Generating resource code
========================
Modified: hurry.resource/trunk/src/hurry/resource/__init__.py
===================================================================
--- hurry.resource/trunk/src/hurry/resource/__init__.py 2008-10-13 19:33:38 UTC (rev 92152)
+++ hurry.resource/trunk/src/hurry/resource/__init__.py 2008-10-13 19:34:48 UTC (rev 92153)
@@ -1,5 +1,5 @@
from hurry.resource.core import (Library, ResourceInclusion, NeededInclusions,
- mode,
+ mode, bottom,
sort_inclusions_topological,
sort_inclusions_by_extension,
generate_code)
Modified: hurry.resource/trunk/src/hurry/resource/core.py
===================================================================
--- hurry.resource/trunk/src/hurry/resource/core.py 2008-10-13 19:33:38 UTC (rev 92152)
+++ hurry.resource/trunk/src/hurry/resource/core.py 2008-10-13 19:34:48 UTC (rev 92153)
@@ -23,7 +23,8 @@
implements(interfaces.IResourceInclusion)
def __init__(self, library, relpath, depends=None,
- supersedes=None, eager_superseder=False, **kw):
+ supersedes=None, eager_superseder=False,
+ bottom=False, **kw):
"""Create a resource inclusion
library - the library this resource is in
@@ -40,13 +41,20 @@
instead will show up.
eager_superseder - even if only part of the requirements are
met, supersede anyway
+ bottom - optionally, indicate that this resource can be
+ safely included on the bottom of the page (just
+ before ``</body>``). This can be used to
+ improve the performance of page loads when javascript
+ resources are in use. Not all javascript-based resources
+ can however be safely included that way.
keyword arguments - different paths that represent the same
resource in different modes (debug, minified, etc),
or alternatively a fully specified ResourceInclusion.
"""
self.library = library
self.relpath = relpath
-
+ self.bottom = bottom
+
assert not isinstance(depends, basestring)
depends = depends or []
self.depends = normalize_inclusions(library, depends)
@@ -128,12 +136,23 @@
def __init__(self):
self._inclusions = []
self._mode = None
+ self._bottom = False
+ self._force_bottom = False
def need(self, inclusion):
self._inclusions.append(inclusion)
def mode(self, mode):
self._mode = mode
+
+ def bottom(self, force=False, disable=False):
+ if disable:
+ self._bottom = False
+ self._force_bottom = False
+ return
+ self._bottom = True
+ if force:
+ self._force_bottom = True
def _sorted_inclusions(self):
return reversed(sorted(self._inclusions, key=lambda i: i.depth()))
@@ -149,25 +168,39 @@
# python's stable sort to keep inclusion order intact
inclusions = sort_inclusions_by_extension(inclusions)
inclusions = remove_duplicates(inclusions)
+
return inclusions
-
+
def render(self):
- result = []
- library_urls = {}
- for inclusion in self.inclusions():
- library = inclusion.library
- # get cached library url
- library_url = library_urls.get(library.name)
- if library_url is None:
- # if we can't find it, recalculate it
- library_url = interfaces.ILibraryUrl(library)
- if not library_url.endswith('/'):
- library_url += '/'
- library_urls[library.name] = library_url
- result.append(render_inclusion(inclusion,
- library_url + inclusion.relpath))
- return '\n'.join(result)
+ return render_inclusions(self.inclusions())
+
+ def render_topbottom(self):
+ inclusions = self.inclusions()
+ # seperate inclusions in top and bottom inclusions if this is needed
+ if self._bottom:
+ top_inclusions = []
+ bottom_inclusions = []
+ if not self._force_bottom:
+ for inclusion in inclusions:
+ if inclusion.bottom:
+ bottom_inclusions.append(inclusion)
+ else:
+ top_inclusions.append(inclusion)
+ else:
+ for inclusion in inclusions:
+ if inclusion.ext() == '.js':
+ bottom_inclusions.append(inclusion)
+ else:
+ top_inclusions.append(inclusion)
+ else:
+ top_inclusions = inclusions
+ bottom_inclusions = []
+
+ library_urls = {}
+ return (render_inclusions(top_inclusions, library_urls),
+ render_inclusions(bottom_inclusions, library_urls))
+
def mode(mode):
"""Set the mode for the currently needed resources.
"""
@@ -175,6 +208,13 @@
interfaces.ICurrentNeededInclusions)()
needed.mode(mode)
+def bottom(force=False):
+ """Try to include resources at the bottom of the page, not just on top.
+ """
+ needed = component.getUtility(
+ interfaces.ICurrentNeededInclusions)()
+ needed.bottom(force)
+
def apply_mode(inclusions, mode):
return [inclusion.mode(mode) for inclusion in inclusions]
@@ -271,6 +311,30 @@
'.js': render_js,
}
+def render_inclusions(inclusions, library_urls=None):
+ """Render a set of inclusions.
+
+ inclusions - the inclusions to render
+ library_urls - optionally a dictionary for maintaining cached library
+ URLs. Doing render_inclusions with the same
+ dictionary can reduce component lookups.
+ """
+ result = []
+ library_urls = library_urls or {}
+ for inclusion in inclusions:
+ library = inclusion.library
+ # get cached library url
+ library_url = library_urls.get(library.name)
+ if library_url is None:
+ # if we can't find it, recalculate it
+ library_url = interfaces.ILibraryUrl(library)
+ if not library_url.endswith('/'):
+ library_url += '/'
+ library_urls[library.name] = library_url
+ result.append(render_inclusion(inclusion,
+ library_url + inclusion.relpath))
+ return '\n'.join(result)
+
def render_inclusion(inclusion, url):
renderer = inclusion_renderers.get(inclusion.ext(), None)
if renderer is None:
Modified: hurry.resource/trunk/src/hurry/resource/interfaces.py
===================================================================
--- hurry.resource/trunk/src/hurry/resource/interfaces.py 2008-10-13 19:33:38 UTC (rev 92152)
+++ hurry.resource/trunk/src/hurry/resource/interfaces.py 2008-10-13 19:34:48 UTC (rev 92153)
@@ -22,6 +22,9 @@
"resource depends on")
rollups = Attribute("A list of potential rollup ResourceInclusions "
"that this resource is part of")
+ bottom = Attribute("A flag. When set to True, this resource "
+ "can be safely included on the bottom of a HTML "
+ "page, just before the </body> tag.")
def ext():
"""Get the filesystem extension of this resource.
@@ -90,6 +93,24 @@
NOTE: there is also a ``hurry.resource.mode`` function which
can be used to set the mode for the currently needed inclusions.
"""
+
+ def bottom(force=False, disable=False):
+ """Control the behavior of ``render_topbottom``.
+
+ If not called or called with ``disable`` set to ``True``,
+ resources will only be included in the top fragment returned
+ by ``render_topbottom``.
+
+ If called without arguments, resource inclusions marked safe
+ to render at the bottom are rendered in the bottom fragment returned
+ by ``render_topbottom``.
+
+ If called with the ``force`` argument set to ``True``, Javascript
+ (``.js``) resource inclusions are always included at the bottom.
+
+ NOTE: there is also a ``hurry.resource.mode`` function which
+ can be used to set the mode for the currently needed inclusions.
+ """
def inclusions():
"""Give all resource inclusions needed.
@@ -100,9 +121,39 @@
def render():
"""Render all resource inclusions for HTML header.
- Returns a HTML snippet that includes the required resource inclusions.
+ Returns a single HTML snippet to be included in the HTML
+ page just after the ``<head>`` tag.
+
+ ``force_bottom`` settings are ignored; everything is always
+ rendered on top.
"""
+
+ def render_topbottom():
+ """Render all resource inclusions into top and bottom snippet.
+ Returns two HTML snippets that include the required resource
+ inclusions, one for the top of the page, one for the bottom.
+
+ if ``bottom`` was not called, behavior is like ``render``;
+ only the top fragment will ever contain things to include, the
+ bottom fragment will be empty.
+
+ if ``bottom`` was called, bottom will include all resource
+ inclusions that have ``bottom`` set to True (safe to include
+ at the bottom of the HTML page), top will contain the rest.
+
+ if ``bottom`` was called with the ``force`` argument set to
+ ``True``, both top and bottom snippet will return content. top
+ will contain all non-javascript resources, and bottom all
+ javascript resources.
+
+ The bottom fragment can be used to speed up page rendering:
+
+ http://developer.yahoo.com/performance/rules.html
+
+ Returns top and bottom HTML fragments.
+ """
+
class ICurrentNeededInclusions(Interface):
def __call__():
"""Return the current needed inclusions object.
More information about the Checkins
mailing list