[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