diff --git a/CHANGELOG.md b/CHANGELOG.md
index d1c27682f04fc41930c12bd2eff2676d3b92fc2f..e58412c8838d57cc0cff4d6ec439c9072a093555 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
-- Add `HeadlessPluginContext` and `Canvas3DRenderer` to be used in Node.js
+- Add `HeadlessPluginContext` and `HeadlessScreenshotHelper` to be used in Node.js
 - Add example `image-renderer`
 - Fix wrong offset when rendering text with orthographic projection
 - Update camera/handle helper when `devicePixelRatio` changes
diff --git a/src/examples/image-renderer/index.ts b/src/examples/image-renderer/index.ts
index ceed7ccc301e2a53a795901f7e79f1f1842ff6c3..4acaa96cd27581bec00a8e88d855f71b83b0e236 100644
--- a/src/examples/image-renderer/index.ts
+++ b/src/examples/image-renderer/index.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2021-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Adam Midlik <midlik@gmail.com>
  *
@@ -10,15 +10,15 @@
  */
 
 import { ArgumentParser } from 'argparse';
-import * as fs from 'fs';
+import fs from 'fs';
 import path from 'path';
 
-import { STYLIZED_POSTPROCESSING } from '../../mol-canvas3d/renderer';
 import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
 import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
 import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
 import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
 import { DefaultPluginSpec } from '../../mol-plugin/spec';
+import { STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
 
 
 interface Args {
diff --git a/src/mol-plugin/headless-plugin-context.ts b/src/mol-plugin/headless-plugin-context.ts
index dd765ecb65d278e5d1b457dc8ae1e4457509c85f..0d547a13425f47726caf49e1697f603958cfc6a8 100644
--- a/src/mol-plugin/headless-plugin-context.ts
+++ b/src/mol-plugin/headless-plugin-context.ts
@@ -1,25 +1,24 @@
 /**
- * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Adam Midlik <midlik@gmail.com>
  */
 
-import * as fs from 'fs';
-
+import fs from 'fs';
 import { Canvas3D } from '../mol-canvas3d/canvas3d';
 import { PostprocessingProps } from '../mol-canvas3d/passes/postprocessing';
-import { Canvas3DRenderer, ImageRendererOptions } from '../mol-canvas3d/renderer';
 import { PluginContext } from './context';
 import { PluginSpec } from './spec';
+import { HeadlessScreenshotHelper, HeadlessScreenshotHelperOptions } from './util/headless-screenshot';
 
 
 /** PluginContext that can be used in Node.js (without DOM) */
 export class HeadlessPluginContext extends PluginContext {
-    renderer: Canvas3DRenderer;
+    renderer: HeadlessScreenshotHelper;
 
-    constructor(spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: ImageRendererOptions) {
+    constructor(spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: HeadlessScreenshotHelperOptions) {
         super(spec);
-        this.renderer = new Canvas3DRenderer(canvasSize, undefined, rendererOptions);
+        this.renderer = new HeadlessScreenshotHelper(canvasSize, undefined, rendererOptions);
         (this.canvas3d as Canvas3D) = this.renderer.canvas3d;
     }
 
diff --git a/src/mol-canvas3d/renderer.ts b/src/mol-plugin/util/headless-screenshot.ts
similarity index 89%
rename from src/mol-canvas3d/renderer.ts
rename to src/mol-plugin/util/headless-screenshot.ts
index 00bf5ed0d114ead669d4c1cae5b39a3395aafa9a..aa98ebad00b15bca25257f21eff45fb453842e05 100644
--- a/src/mol-canvas3d/renderer.ts
+++ b/src/mol-plugin/util/headless-screenshot.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Jesse Liang <jesse.liang@rcsb.org>
@@ -8,23 +8,22 @@
  * @author Adam Midlik <midlik@gmail.com>
  */
 
-import * as fs from 'fs';
+import fs from 'fs';
 import path from 'path';
-
 import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import is done by LazyImports
 import { type PNG } from 'pngjs'; // Only import type here, the actual import is done by LazyImports
 
-import { createContext } from '../mol-gl/webgl/context';
-import { AssetManager } from '../mol-util/assets';
-import { ColorNames } from '../mol-util/color/names';
-import { PixelData } from '../mol-util/image';
-import { InputObserver } from '../mol-util/input/input-observer';
-import { LazyImports } from '../mol-util/lazy-imports';
-import { ParamDefinition } from '../mol-util/param-definition';
-import { Canvas3D, Canvas3DContext, Canvas3DProps, DefaultCanvas3DParams } from './canvas3d';
-import { ImagePass, ImageProps } from './passes/image';
-import { Passes } from './passes/passes';
-import { PostprocessingParams, PostprocessingProps } from './passes/postprocessing';
+import { Canvas3D, Canvas3DContext, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
+import { ImagePass, ImageProps } from '../../mol-canvas3d/passes/image';
+import { Passes } from '../../mol-canvas3d/passes/passes';
+import { PostprocessingParams, PostprocessingProps } from '../../mol-canvas3d/passes/postprocessing';
+import { createContext } from '../../mol-gl/webgl/context';
+import { AssetManager } from '../../mol-util/assets';
+import { ColorNames } from '../../mol-util/color/names';
+import { PixelData } from '../../mol-util/image';
+import { InputObserver } from '../../mol-util/input/input-observer';
+import { LazyImports } from '../../mol-util/lazy-imports';
+import { ParamDefinition } from '../../mol-util/param-definition';
 
 
 const lazyImports = LazyImports.create('gl', 'jpeg-js', 'pngjs') as {
@@ -34,7 +33,7 @@ const lazyImports = LazyImports.create('gl', 'jpeg-js', 'pngjs') as {
 };
 
 
-export type ImageRendererOptions = {
+export type HeadlessScreenshotHelperOptions = {
     webgl?: WebGLContextAttributes,
     canvas?: Partial<Canvas3DProps>,
     imagePass?: Partial<ImageProps>,
@@ -48,11 +47,11 @@ export type RawImageData = {
 
 
 /** To render Canvas3D when running in Node.js (without DOM) */
-export class Canvas3DRenderer {
+export class HeadlessScreenshotHelper {
     readonly canvas3d: Canvas3D;
     readonly imagePass: ImagePass;
 
-    constructor(readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: ImageRendererOptions) {
+    constructor(readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: HeadlessScreenshotHelperOptions) {
         if (canvas3d) {
             this.canvas3d = canvas3d;
         } else {
diff --git a/src/mol-util/assets.ts b/src/mol-util/assets.ts
index 7a14df662a9d8741123f398bc0d925bf5dd17500..9c1f8cb71921bc675ba2e73914c3716e2ac65c05 100644
--- a/src/mol-util/assets.ts
+++ b/src/mol-util/assets.ts
@@ -9,7 +9,7 @@ import { UUID } from './uuid';
 import { iterableToArray } from '../mol-data/util';
 import { ajaxGet, DataType, DataResponse, readFromFile } from './data-source';
 import { Task } from '../mol-task';
-import { File_ as File } from './nodejs-browser-io';
+import { File_ as File } from './nodejs-shims';
 
 export { AssetManager, Asset };
 
diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts
index 37f6cb983ee2907aa180d333d3a268232662071a..753fbc36e11a9d7b1cc23f514c0f65c3347e2941 100644
--- a/src/mol-util/data-source.ts
+++ b/src/mol-util/data-source.ts
@@ -1,19 +1,24 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Adam Midlik <midlik@gmail.com>
  *
  * Adapted from LiteMol
  */
 
-import * as fs from 'fs';
-
-import { Task, RuntimeContext } from '../mol-task';
-import { unzip, ungzip } from './zip/zip';
 import { utf8Read } from '../mol-io/common/utf8';
-import { AssetManager, Asset } from './assets';
-import { RUNNING_IN_NODEJS, File_ as File, XMLHttpRequest_ as XMLHttpRequest } from './nodejs-browser-io';
+import { RuntimeContext, Task } from '../mol-task';
+import { Asset, AssetManager } from './assets';
+import { LazyImports } from './lazy-imports';
+import { File_ as File, RUNNING_IN_NODEJS, XMLHttpRequest_ as XMLHttpRequest } from './nodejs-shims';
+import { ungzip, unzip } from './zip/zip';
+
+
+const lazyImports = LazyImports.create('fs') as {
+    'fs': typeof import ('fs'),
+};
 
 
 export enum DataCompressionMethod {
@@ -306,7 +311,7 @@ function ajaxGetInternal_file_NodeJS<T extends DataType>(title: string | undefin
     if (!RUNNING_IN_NODEJS) throw new Error('This function should only be used when running in Node.js');
     if (!url.startsWith('file://')) throw new Error('This function is only for URLs with protocol file://');
     const filename = url.substring('file://'.length);
-    const data = fs.readFileSync(filename);
+    const data = lazyImports.fs.readFileSync(filename);
     const file = new File([data], 'raw-data');
     return readFromFile(file, type);
 }
diff --git a/src/mol-util/lazy-imports.ts b/src/mol-util/lazy-imports.ts
index 61e2619e3bb945bb40867db22055406825241fb1..774e9a4d110f2761a58928b69f24353f11a4c2d4 100644
--- a/src/mol-util/lazy-imports.ts
+++ b/src/mol-util/lazy-imports.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Adam Midlik <midlik@gmail.com>
  *
diff --git a/src/mol-util/nodejs-browser-io.ts b/src/mol-util/nodejs-shims.ts
similarity index 87%
rename from src/mol-util/nodejs-browser-io.ts
rename to src/mol-util/nodejs-shims.ts
index 973113efa5964877b7429437f04ff4706bf691d8..fd4304860f5fe386656e28a6f01865dbe54e91e9 100644
--- a/src/mol-util/nodejs-browser-io.ts
+++ b/src/mol-util/nodejs-shims.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Adam Midlik <midlik@gmail.com>
  *
@@ -8,8 +8,8 @@
  */
 
 
-/** Determines whether the current code is running in Node.js or in a browser */
-export const RUNNING_IN_NODEJS = typeof document === 'undefined';
+/** Determines whether the current code is running in Node.js */
+export const RUNNING_IN_NODEJS = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
 
 /** Like `XMLHttpRequest` but works also in Node.js */
 export const XMLHttpRequest_ = getXMLHttpRequest();