diff --git a/package.json b/package.json
index 0c298eee8a47dc634c442c30fbe8f0cc92acf38e..869201c487babe289c3e3789b2d4fc8d6a97e151 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,6 @@
     "react-test-renderer": "16.2.0",
     "react-tooltip": "3.8.0",
     "react-virtualized": "9.20.1",
-    "react-visibility-sensor": "5.0.2",
     "redux": "3.7.2",
     "redux-mock-store": "^1.2.3",
     "redux-throttle": "0.1.1",
diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx
index 3ae872e8ad795f2d3b24d1eb4d6b2bec60e5a210..5af6fb15956cac258263186d819a0d2bba3e375e 100644
--- a/src/components/library-item/library-item.jsx
+++ b/src/components/library-item/library-item.jsx
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import Box from '../box/box.jsx';
-import ScratchImage from '../scratch-image/scratch-image.jsx';
 import styles from './library-item.css';
 import classNames from 'classnames';
 
@@ -36,12 +35,10 @@ class LibraryItemComponent extends React.PureComponent {
                             />
                         </div>
                     ) : null}
-                    {this.props.iconSource ? (
-                        <ScratchImage
-                            className={styles.featuredImage}
-                            imageSource={this.props.iconSource}
-                        />
-                    ) : null}
+                    <img
+                        className={styles.featuredImage}
+                        src={this.props.iconURL}
+                    />
                 </div>
                 {this.props.insetIconURL ? (
                     <div className={styles.libraryItemInsetImageContainer}>
@@ -124,9 +121,9 @@ class LibraryItemComponent extends React.PureComponent {
                 {/* Layers of wrapping is to prevent layout thrashing on animation */}
                 <Box className={styles.libraryItemImageContainerWrapper}>
                     <Box className={styles.libraryItemImageContainer}>
-                        <ScratchImage
+                        <img
                             className={styles.libraryItemImage}
-                            imageSource={this.props.iconSource}
+                            src={this.props.iconURL}
                         />
                     </Box>
                 </Box>
@@ -149,7 +146,7 @@ LibraryItemComponent.propTypes = {
     extensionId: PropTypes.string,
     featured: PropTypes.bool,
     hidden: PropTypes.bool,
-    iconSource: ScratchImage.ImageSourcePropType,
+    iconURL: PropTypes.string,
     insetIconURL: PropTypes.string,
     internetConnectionRequired: PropTypes.bool,
     name: PropTypes.oneOfType([
diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx
index 89e820f1b4452abd64b56834d7ebada2c21c3352..60f65a06d5760df690a9bbea8788b426e1629a23 100644
--- a/src/components/library/library.jsx
+++ b/src/components/library/library.jsx
@@ -9,7 +9,6 @@ import Modal from '../../containers/modal.jsx';
 import Divider from '../divider/divider.jsx';
 import Filter from '../filter/filter.jsx';
 import TagButton from '../../containers/tag-button.jsx';
-import storage from '../../lib/storage';
 
 import styles from './library.css';
 
@@ -29,47 +28,6 @@ const messages = defineMessages({
 const ALL_TAG = {tag: 'all', intlLabel: messages.allTag};
 const tagListPrefix = [ALL_TAG];
 
-/**
- * Find the AssetType which corresponds to a particular file extension. For example, 'png' => AssetType.ImageBitmap.
- * @param {string} fileExtension - the file extension to look up.
- * @returns {AssetType} - the AssetType corresponding to the extension, if any.
- */
-const getAssetTypeForFileExtension = function (fileExtension) {
-    const compareOptions = {
-        sensitivity: 'accent',
-        usage: 'search'
-    };
-    for (const assetTypeId in storage.AssetType) {
-        const assetType = storage.AssetType[assetTypeId];
-        if (fileExtension.localeCompare(assetType.runtimeFormat, compareOptions) === 0) {
-            return assetType;
-        }
-    }
-};
-
-/**
- * Figure out an `imageSource` (URI or asset ID & type) for a library item's icon.
- * @param {object} item - either a library item or one of a library item's costumes.
- * @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`.
- */
-const getItemImageSource = function (item) {
-    if (item.rawURL) {
-        return {
-            uri: item.rawURL
-        };
-    }
-
-    // TODO: adjust libraries to be more storage-friendly; don't use split() here.
-    const md5 = item.md5 || item.baseLayerMD5;
-    if (md5) {
-        const [assetId, fileExtension] = md5.split('.');
-        return {
-            assetId: assetId,
-            assetType: getAssetTypeForFileExtension(fileExtension)
-        };
-    }
-};
-
 class LibraryComponent extends React.Component {
     constructor (props) {
         super(props);
@@ -204,10 +162,8 @@ class LibraryComponent extends React.Component {
                     })}
                     ref={this.setFilteredDataRef}
                 >
-                    {this.getFilteredData().map((dataItem, index) => {
-                        const iconSource = getItemImageSource(dataItem);
-                        const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource);
-                        return (<LibraryItem
+                    {this.getFilteredData().map((dataItem, index) => (
+                        <LibraryItem
                             bluetoothRequired={dataItem.bluetoothRequired}
                             collaborator={dataItem.collaborator}
                             description={dataItem.description}
@@ -215,8 +171,9 @@ class LibraryComponent extends React.Component {
                             extensionId={dataItem.extensionId}
                             featured={dataItem.featured}
                             hidden={dataItem.hidden}
-                            iconSource={iconSource}
-                            icons={icons}
+                            iconMd5={dataItem.md5}
+                            iconRawURL={dataItem.rawURL}
+                            icons={dataItem.json && dataItem.json.costumes}
                             id={index}
                             insetIconURL={dataItem.insetIconURL}
                             internetConnectionRequired={dataItem.internetConnectionRequired}
@@ -225,8 +182,8 @@ class LibraryComponent extends React.Component {
                             onMouseEnter={this.handleMouseEnter}
                             onMouseLeave={this.handleMouseLeave}
                             onSelect={this.handleSelect}
-                        />);
-                    })}
+                        />
+                    ))}
                 </div>
             </Modal>
         );
diff --git a/src/components/scratch-image/scratch-image.jsx b/src/components/scratch-image/scratch-image.jsx
deleted file mode 100644
index 15abe9d3fca411ef8012f9865ffc0ab9fdf343f3..0000000000000000000000000000000000000000
--- a/src/components/scratch-image/scratch-image.jsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import VisibilitySensor from 'react-visibility-sensor';
-
-import storage from '../../lib/storage';
-
-class ScratchImage extends React.PureComponent {
-    static init () {
-        this._maxParallelism = 6;
-        this._currentJobs = 0;
-        this._pendingImages = new Set();
-    }
-
-    static loadPendingImages () {
-        if (this._currentJobs >= this._maxParallelism) {
-            // already busy
-            return;
-        }
-
-        // Find the first visible image. If there aren't any, find the first non-visible image.
-        let nextImage;
-        for (const image of this._pendingImages) {
-            if (image.isVisible) {
-                nextImage = image;
-                break;
-            } else {
-                nextImage = nextImage || image;
-            }
-        }
-
-        // If we found an image to load:
-        // 1) Remove it from the queue
-        // 2) Load the image
-        // 3) Pump the queue again
-        if (nextImage) {
-            this._pendingImages.delete(nextImage);
-            const imageSource = nextImage.props.imageSource;
-            ++this._currentJobs;
-            storage
-                .load(imageSource.assetType, imageSource.assetId)
-                .then(asset => {
-                    if (!nextImage.wasUnmounted) {
-                        const dataURI = asset.encodeDataURI();
-
-                        nextImage.setState({
-                            imageURI: dataURI
-                        });
-                    }
-                    --this._currentJobs;
-                    this.loadPendingImages();
-                });
-        }
-    }
-
-    constructor (props) {
-        super(props);
-        this.state = {};
-        Object.assign(this.state, this._loadImageSource(props.imageSource));
-    }
-    componentWillReceiveProps (nextProps) {
-        const newState = this._loadImageSource(nextProps.imageSource);
-        this.setState(newState);
-    }
-    componentWillUnmount () {
-        this.wasUnmounted = true;
-        ScratchImage._pendingImages.delete(this);
-    }
-    /**
-     * Calculate the state changes necessary to load the image specified in the provided source info. If the component
-     * is mounted, call setState() with the return value of this function. If the component has not yet mounted, use
-     * the return value of this function as initial state for the component.
-     *
-     * @param {object} imageSource - the new source for the image, including either assetId or URI
-     * @returns {object} - the new state values, if any.
-     */
-    _loadImageSource (imageSource) {
-        if (imageSource) {
-            if (imageSource.uri) {
-                ScratchImage._pendingImages.delete(this);
-                return {
-                    imageURI: imageSource.uri,
-                    lastRequestedAsset: null
-                };
-            }
-            if (this.state.lastRequestedAsset !== imageSource.assetId) {
-                ScratchImage._pendingImages.add(this);
-                return {
-                    lastRequestedAsset: imageSource.assetId
-                };
-            }
-        }
-        // Nothing to do - don't change any state.
-        return {};
-    }
-    render () {
-        const {
-            src: _src,
-            imageSource: _imageSource,
-            ...imgProps
-        } = this.props;
-        return (
-            <VisibilitySensor
-                intervalCheck
-                scrollCheck
-            >
-                {
-                    ({isVisible}) => {
-                        this.isVisible = isVisible;
-                        ScratchImage.loadPendingImages();
-                        return (
-                            <img
-                                src={this.state.imageURI}
-                                {...imgProps}
-                            />
-                        );
-                    }
-                }
-            </VisibilitySensor>
-        );
-    }
-}
-
-ScratchImage.ImageSourcePropType = PropTypes.oneOfType([
-    PropTypes.shape({
-        assetId: PropTypes.string.isRequired,
-        assetType: PropTypes.oneOf(Object.values(storage.AssetType)).isRequired
-    }),
-    PropTypes.shape({
-        uri: PropTypes.string.isRequired
-    })
-]);
-
-ScratchImage.propTypes = {
-    imageSource: ScratchImage.ImageSourcePropType.isRequired
-};
-
-ScratchImage.init();
-
-export default ScratchImage;
diff --git a/src/containers/library-item.jsx b/src/containers/library-item.jsx
index 0f770546d301904115e42cc27430f5f00c91ae89..6177b916fd69c00107decb03b1d0de13073194ae 100644
--- a/src/containers/library-item.jsx
+++ b/src/containers/library-item.jsx
@@ -75,17 +75,21 @@ class LibraryItem extends React.PureComponent {
         const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length;
         this.setState({iconIndex: nextIconIndex});
     }
-    curIconSource () {
+    curIconMd5 () {
         if (this.props.icons &&
             this.state.isRotatingIcon &&
             this.state.iconIndex < this.props.icons.length &&
-            this.props.icons[this.state.iconIndex]) {
-            return this.props.icons[this.state.iconIndex];
+            this.props.icons[this.state.iconIndex] &&
+            this.props.icons[this.state.iconIndex].baseLayerMD5) {
+            return this.props.icons[this.state.iconIndex].baseLayerMD5;
         }
-        return this.props.iconSource;
+        return this.props.iconMd5;
     }
     render () {
-        const iconSource = this.curIconSource();
+        const iconMd5 = this.curIconMd5();
+        const iconURL = iconMd5 ?
+            `https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` :
+            this.props.iconRawURL;
         return (
             <LibraryItemComponent
                 bluetoothRequired={this.props.bluetoothRequired}
@@ -95,7 +99,8 @@ class LibraryItem extends React.PureComponent {
                 extensionId={this.props.extensionId}
                 featured={this.props.featured}
                 hidden={this.props.hidden}
-                iconSource={iconSource}
+                iconURL={iconURL}
+                icons={this.props.icons}
                 id={this.props.id}
                 insetIconURL={this.props.insetIconURL}
                 internetConnectionRequired={this.props.internetConnectionRequired}
@@ -122,8 +127,13 @@ LibraryItem.propTypes = {
     extensionId: PropTypes.string,
     featured: PropTypes.bool,
     hidden: PropTypes.bool,
-    iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
-    icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
+    iconMd5: PropTypes.string,
+    iconRawURL: PropTypes.string,
+    icons: PropTypes.arrayOf(
+        PropTypes.shape({
+            baseLayerMD5: PropTypes.string
+        })
+    ),
     id: PropTypes.number.isRequired,
     insetIconURL: PropTypes.string,
     internetConnectionRequired: PropTypes.bool,
diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js
index 6b6a021dda9e48acc3932f84e01417b017b0d363..0d1a784f6f7fdbdc259ad1fa561e3bd0f8d7c297 100644
--- a/test/integration/costumes.test.js
+++ b/test/integration/costumes.test.js
@@ -193,11 +193,7 @@ describe('Working with costumes', () => {
             .mouseMove(abbyElement)
             .perform();
         // wait for one of Abby's alternate costumes to appear
-        const src1 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
-        await driver.sleep(300);
-        const src2 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
-        const sourcesMatch = (src1 === src2);
-        await expect(sourcesMatch).toBeFalsy(); // 'src' attribute should have changed by now
+        await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]');
         const logs = await getLogs();
         await expect(logs).toEqual([]);
     });