diff --git a/CHANGELOG.md b/CHANGELOG.md index 0eae5e0ded..fbb0ab9db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ way to update this template, but currently, we follow a pattern: ## Upcoming version 2018-09-XX +* [change] Improve performance of public pages. Image assets are optimized and lazy loading is + applied to images in SectionLocation and ListingCard. Read + [documentation](./docs/improving-performance.md) for implementation details. + [#936](https://github.com/sharetribe/flex-template-web/pull/936) * [change] Update sharetribe-scripts. **cssnext** (used previously in sharetribe-scripts) has been deprecated. Now **postcss-preset-env** is used instead with stage 3 + custom media queries and nesting-rules. If this change breaks your styling, you could still use v1.1.2. The next version of diff --git a/docs/README.md b/docs/README.md index d67c86517c..d58cbd766d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -51,6 +51,7 @@ Documentation for specific topics can be found in the following files: * [Original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md) * [Customization checklist](customization-checklist.md) * [Icons](icons.md) +* [Improving performance](improving-performance.md) The application was bootstrapped with a forked version of [create-react-app](https://github.com/facebookincubator/create-react-app). While most of the diff --git a/docs/improving-performance.md b/docs/improving-performance.md new file mode 100644 index 0000000000..1d23aa4078 --- /dev/null +++ b/docs/improving-performance.md @@ -0,0 +1,74 @@ +# Improving page rendering performance + +When we think about page speed there are actually two different scenarios that we need to address: + +* The speed of initial page load and possible reloads after that +* The speed of changing the page within Single page application (SPA) + +The first one is usually a slower process. A browser needs to load all the HTML, CSS, JavaScript, +and images - and then it needs to understand and execute those files, calculate layout, paint +components and finally composite the whole view. The initial page load is the slowest since the +consequent page reloads can take benefit from browser caches. + +SPAs can improve from that since they don't necessarily need to download anymore JavaScript, HTML, +or CSS - already downloaded JavaScript might be enough for rendering consequent pages when a user +wants to navigate to another page. Most of the time SPAs just fetch data for that page. + +These two UX scenarios might also conflict with each other. If all the JavaScript is in one big +bundle, page changes within a SPA are fast. However, downloading and evaluating a big JavaScript +file is slowing initial page rendering down. Even though users rarely experience the full initial +page load speed when they use an SPA like Flex Template for Web, it is good to keep track of that +speed. Especially since that is what search engine bots are experiencing and therefore it might +affect your page rank. + +Read more about +[website performance](https://developers.google.com/web/fundamentals/performance/why-performance-matters/). + +We haven't yet implemented code splitting to reduce initial page rendering time, but there're other +improvements that could be done to improve both cases of page rendering. + +## Check page performance + +The first step is, of course, to start measuring performance. +[Lighthouse](https://developers.google.com/web/tools/lighthouse/) is a good tool to check rendering +performance. At least check those pages that are visible to unauthenticated users (e.g. landing +page, search page, listing page, about page and other static pages). + +Lighthouse will give you some tips about how to improve performance and other aspects that website +developers should think about. + +## Optimize image sizes + +If your page is showing images, you should check that the image size is not bigger than what is +needed. So, adjusting image dimensions is the first step, but you should also think about image +quality, advanced rendering options and possibly serving those images from CDN instead of from +within your web app. + +Quick checklist: + +* Check that the actual dimensions of an image match with DOM element's dimensions. +* Lighthouse suggests that image compression level should be 85% or lower. + [Read more](https://developers.google.com/web/tools/lighthouse/audits/optimize-images) +* Good rule-of-thumb is that use JPEG for images and photos, where PNG is better for graphics, such + as logos, graphs and illustrations. +* If you are using JPEG images, think about saving them as progressive JPEGs. + [Read more](https://cloudinary.com/blog/progressive_jpegs_and_green_martians) + + [Photoshop guide](https://helpx.adobe.com/photoshop-elements/using/optimizing-images-jpeg-format.html) +* If you are using PNG images, consider running them through PNG optimizers to reduce file size. + Plenty of options available, one example is [TinyPNG.com](https://tinypng.com) +* Think about serving images and other static assets from some CDN. + [Read more.](https://www.smashingmagazine.com/2017/04/content-delivery-network-optimize-images/) + +## Lazy load off-screen images and other components + +Another way of dealing with images is to lazy load those images that are not visible inside an +initially rendered part of the screen. Lazy loading these off-screen images can be done with helper +function: `lazyLoadWithDimensions` (from `util/contextHelpers/`). Check `SectionLocations` component +for details. + +## Use sparse fields + +Another way to reduce the amount of data that is fetched from API is sparse fields. This is a +relatively new feature and Flex template app has not yet leveraged it fully, but it is created to +reduce unnecessary data and speed up rendering. You can read more from +[Flex API docs](https://flex-docs.sharetribe.com/#sparse-attributes). diff --git a/src/assets/background-1440.jpg b/src/assets/background-1440.jpg index f37bf1724f..8b777b8317 100644 Binary files a/src/assets/background-1440.jpg and b/src/assets/background-1440.jpg differ diff --git a/src/components/ListingCard/ListingCard.js b/src/components/ListingCard/ListingCard.js index c80742f6fc..c68a1a7aa7 100644 --- a/src/components/ListingCard/ListingCard.js +++ b/src/components/ListingCard/ListingCard.js @@ -1,14 +1,15 @@ -import React from 'react'; +import React, { Component } from 'react'; import { string, func } from 'prop-types'; import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; import classNames from 'classnames'; -import { NamedLink, ResponsiveImage } from '../../components'; +import { lazyLoadWithDimensions } from '../../util/contextHelpers'; import { propTypes } from '../../util/types'; import { formatMoney } from '../../util/currency'; import { ensureListing, ensureUser } from '../../util/data'; import { richText } from '../../util/richText'; import { createSlug } from '../../util/urlHelpers'; import config from '../../config'; +import { NamedLink, ResponsiveImage } from '../../components'; import css from './ListingCard.css'; @@ -33,6 +34,13 @@ const priceData = (price, intl) => { return {}; }; +class ListingImage extends Component { + render() { + return ; + } +} +const LazyImage = lazyLoadWithDimensions(ListingImage, { loadAfterInitialRendering: 3000 }); + export const ListingCardComponent = props => { const { className, rootClassName, intl, listing, renderSizes, setActiveListing } = props; const classes = classNames(rootClassName || css.root, className); @@ -55,7 +63,7 @@ export const ListingCardComponent = props => { onMouseLeave={() => setActiveListing(null)} >
-
- ; + } +} +const LazyImage = lazyLoadWithDimensions(LocationImage); + const locationLink = (name, image, searchQuery) => { const nameText = {name}; return (
- {name} +
diff --git a/src/components/SectionLocations/images/location_helsinki.jpg b/src/components/SectionLocations/images/location_helsinki.jpg index b0e06a058d..24b3a96337 100644 Binary files a/src/components/SectionLocations/images/location_helsinki.jpg and b/src/components/SectionLocations/images/location_helsinki.jpg differ diff --git a/src/components/SectionLocations/images/location_rovaniemi.jpg b/src/components/SectionLocations/images/location_rovaniemi.jpg index fad40f61bf..3975c1da83 100644 Binary files a/src/components/SectionLocations/images/location_rovaniemi.jpg and b/src/components/SectionLocations/images/location_rovaniemi.jpg differ diff --git a/src/components/SectionLocations/images/location_ruka.jpg b/src/components/SectionLocations/images/location_ruka.jpg index 14ba7918ed..3374bcd886 100644 Binary files a/src/components/SectionLocations/images/location_ruka.jpg and b/src/components/SectionLocations/images/location_ruka.jpg differ diff --git a/src/containers/AboutPage/AboutPage.js b/src/containers/AboutPage/AboutPage.js index 04a7111125..1bb49a88fe 100644 --- a/src/containers/AboutPage/AboutPage.js +++ b/src/containers/AboutPage/AboutPage.js @@ -12,7 +12,7 @@ import { } from '../../components'; import css from './AboutPage.css'; -import image from './about-us-1440.jpg'; +import image from './about-us-1056.jpg'; const AboutPage = () => { const { siteTwitterHandle, siteFacebookPage } = config; diff --git a/src/containers/AboutPage/about-us-1056.jpg b/src/containers/AboutPage/about-us-1056.jpg new file mode 100644 index 0000000000..3f21607346 Binary files /dev/null and b/src/containers/AboutPage/about-us-1056.jpg differ diff --git a/src/containers/AboutPage/about-us-1440.jpg b/src/containers/AboutPage/about-us-1440.jpg deleted file mode 100644 index f37bf1724f..0000000000 Binary files a/src/containers/AboutPage/about-us-1440.jpg and /dev/null differ diff --git a/src/util/contextHelpers.js b/src/util/contextHelpers.js index 03d1c2612b..a72b0da6c5 100644 --- a/src/util/contextHelpers.js +++ b/src/util/contextHelpers.js @@ -75,11 +75,13 @@ export const withViewport = Component => { * the shape `{ width: 600, height: 400}`. * * @param {React.Component} Component to be wrapped by this HOC - * @param {Object} options pass in options like maxWidth and maxHeight. + * @param {Object} options pass in options like maxWidth and maxHeight. To load component after + * initial rendering has passed or after user has interacted with the window (e.g. scrolled), + * use`loadAfterInitialRendering: 1500` (value should be milliseconds). * * @return {Object} HOC component which knows its dimensions */ -export const lazyLoadWithDimensions = (Component, options) => { +export const lazyLoadWithDimensions = (Component, options = {}) => { // The resize event is flooded when the browser is resized. We'll // use a small timeout to throttle changing the viewport since it // will trigger rerendering. @@ -96,6 +98,7 @@ export const lazyLoadWithDimensions = (Component, options) => { super(props); this.element = null; this.defaultRenderTimeout = null; + this.afterRenderTimeout = null; this.state = { width: 0, height: 0 }; @@ -112,6 +115,15 @@ export const lazyLoadWithDimensions = (Component, options) => { this.defaultRenderTimeout = window.setTimeout(() => { if (this.isElementNearViewport(0)) { this.setDimensions(); + } else { + const loadAfterInitialRendering = options.loadAfterInitialRendering; + if (typeof loadAfterInitialRendering === 'number') { + this.afterRenderTimeout = window.setTimeout(() => { + window.requestAnimationFrame(() => { + this.setDimensions(); + }); + }, loadAfterInitialRendering); + } } }, RENDER_WAIT_MS); } @@ -121,11 +133,18 @@ export const lazyLoadWithDimensions = (Component, options) => { window.removeEventListener('resize', this.handleWindowResize); window.removeEventListener('orientationchange', this.handleWindowResize); window.clearTimeout(this.defaultRenderTimeout); + + if (this.afterRenderTimeout) { + window.clearTimeout(this.afterRenderTimeout); + } } handleWindowResize() { - if (this.isElementNearViewport(NEAR_VIEWPORT_MARGIN)) { - this.setDimensions(); + const shouldLoadToImproveScrolling = typeof options.loadAfterInitialRendering === 'number'; + if (this.isElementNearViewport(NEAR_VIEWPORT_MARGIN) || shouldLoadToImproveScrolling) { + window.requestAnimationFrame(() => { + this.setDimensions(); + }); } } @@ -159,7 +178,10 @@ export const lazyLoadWithDimensions = (Component, options) => { const { maxWidth, maxHeight } = options; const maxWidthMaybe = maxWidth ? { maxWidth } : {}; const maxHeightMaybe = maxHeight ? { maxHeight } : {}; - const style = { width: '100%', height: '100%', ...maxWidthMaybe, ...maxHeightMaybe }; + const style = + maxWidth || maxHeight + ? { width: '100%', height: '100%', ...maxWidthMaybe, ...maxHeightMaybe } + : { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }; return (